-
페치 조인(fetch join)JPA/JPQL 2023. 8. 9. 13:53
페치 조인은 JPA에서 매우 중요한 개념이고 실무에서도 너무 중요하다고 한다.
페치 조인이란?
- SQL 조인의 종류가 아님
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 기능
- join fetch 명령어 사용
엔티티 페치 조인 - @ManyToONE에서의 페치 조인
회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)
[JQPL에서의 페치 조인]
select m from Member m join fetch m.team
[실제 실행 SQL]
select M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID
Team teamA = new Team(); teamA.setName("팀A"); em.persist(teamA); Team teamB = new Team(); teamB.setName("팀B"); em.persist(teamB); Member member1 = new Member(); member1.setUsername("member1"); member1.setTeam(teamA); em.persist(member1); Member member2 = new Member(); member2.setUsername("member2"); member2.setTeam(teamA); em.persist(member2); Member member3 = new Member(); member3.setUsername("member3"); member3.setTeam(teamA); em.persist(member3); em.flush(); em.clear(); List<Member> result = em.createQuery("select m from Member m", Member.class) .getResultList(); for(Member member : result) { System.out.println("member = " + member.getUsername() + "," + member.getTeam().getName()); } tx.commit();
위의 코드를 보면 em.createQuery에서 select 쿼리가 한 번 나가게 된다.
그런데 조인을 해주지 않았기 때문에 루프를 돌 때마다 team을 호출해서 쿼리가 나가게 된다.
첫 번째 member에서는 teamA라는 team을 가져오는 쿼리를 날린다. 두 번째 member는 teamA라는 동일한 팀이기 때문에 쿼리를 날리지 않고 영속성 컨텍스트에 저장된 1차 캐시를 가져온다. 세 번째 member는 teamB기 때문에 다시 쿼리를 날려서 teamB를 찾아오게 된다. 최악의 경우에는 쿼리가 4번 나갈 수가 있고, 이렇게 쿼리가 계속 나가는 것은 성능 상 좋지 않다.
이 문제가 바로 N + 1 문제이다. 회원이 100명이면 team을 찾는 쿼리가 100번 나가게 될 것이고, 처음 select 문을 포함해서 101번 쿼리가 나가는 것이다.
이 문제를 해결하기 위해 fetch join을 사용한다.
"select m From Member m join fetch m.team"
이렇게 페치 조인을 사용하면 쿼리 한 번에 member랑 team을 다 가져온다. 그래서 team을 호출할 때에도 프록시가 아니라 미리 가져온 진짜 데이터를 가지고 올 수 있는 것이다.
컬렉션 패치 조인
일대다 관계나 컬렉션 페치 조인을 설명할 것이다.
[JPQL]
select t from Team t join fetch t.members
where t.name = '팀A'
[실제 SQL문]
SELECT T.*, M.*
FROM TEAM T
NNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀A'
List<Team> result = em.createQuery("select t from Team t join fetch t.members" , Team.class) .getResultList(); for(Team team : result) { System.out.println("member = " + team.getName() + "," + team.getMembers().size()); for (Member member : team.getMembers()) { System.out.println("-> username = " + member.getUsername() + ", member = " + member); } }
member와 team은 위와 똑같이 설정하고 fetch join을 이용해서 찾아온 team들의 이름과 team의 member 개수를 출력해보려고 한다.
출력 결과
team = 팀A, member = 2
-> member = Member{id=1, username='member1', age=0}
-> member = Member{id=2, username='member2', age=0}
team = 팀A, member = 2
-> member = Member{id=1, username='member1', age=0}
-> member = Member{id=2, username='member2', age=0}
team = 팀B, member = 1
-> member = Member{id=3, username='member3', age=0}
출력 결과를 보면 팀A가 2번 출력되었다. 그 이유는 팀A는 member를 2명 가지고 있기 때문에 JPA 입장에서는 row를 2개 반환하는 것이다. 팀A가 다른 게 아니라 완전히 같은 것임에도 JPA 정책 상 중복으로 반환된다.
일대다 관계에서 join할 때 데이터가 중복돼서 나타날 수 있는 것이다.
하지만 스프링 3.0 이상부터는 하이버네이트 6 버전을 사용하고 하이버네이트 6 버전에는 페치 조인 사용 시 자동으로 중복 제거를 해준다.
페치 조인과 DISTINCT
SQL에 중복 제거 기능으로 DISTINCT가 있긴 하지만 이걸로 완벽하게 중복을 제거하진 못한다.
그래서 JPQL의 DISTINCT는 2가지 기능을 제공한다.
1. SQL에 DISTINCT를 추가
2. 애플리케이션에서 엔티티 중복 제거
SQL에 DISTINCT를 추가하는 기능은 데이터의 모든 속성이 같아야지만 중복을 제거하기 때문에 애플리케이션에서 중복 제거를 시도한다.
즉, 같은 식별자를 가진 Team 엔티티를 제거하는 것이다.
List<Team> result = em.createQuery("select distinct t from Team t join fetch t.members" , Team.class) .getResultList();
JPQL 쿼리문에 distinct를 넣어주면 SQL에 DISTINCT도 추가가 되며 동시에 애플리케이션에 받아올 때 엔티티의 중복도 제거해준다.
컬렉션 조인(일대다)는 데이터가 증가해서 중복될 수 있지만 다대일 조인에서는 그럴 일이 없다.
페치 조인과 일반 조인의 차이
List<Team> result = em.createQuery("select t from Team t join t.members", Team.class) .getResultList();
위의 쿼리에서 fetch를 빼고 그냥 join을 해보면 select 절에서 team만 가져오는 것을 볼 수 있다.
fetch join이 아니기 때문에 그냥 join만 해주고 데이터를 가져올 때에는 team에 대한 데이터만 가져오고 members에 대한 데이터는 가져오지 않는다.
이후에 데이터를 가져올 때 쿼리가 계속 날라가서 데이터를 가져와야 한다.
그렇기 때문에 한 번에 원하는 데이터들을 가지고 오는 fetch join을 사용하는 것을 추천한다.
'JPA > JPQL' 카테고리의 다른 글
엔티티를 직접 사용하기 (0) 2023.08.09 페치 조인의 한계 (0) 2023.08.09 경로 표현식 (0) 2023.08.09 조건식 (CASE) (0) 2023.07.14 서브 쿼리 (0) 2023.07.14