JPQL - 페치 조인(Fetch Join)

2023. 10. 12. 16:37JPA

반응형

페치 조인은  SQL에서 이야기하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 이것은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데 join fetch 명령어로 사용할 수 있다.

엔티티 패치 조인

패치 조인을 사용해서 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회하는 JPQL을 보자.

select m
from Member m join fetch m.team

예제를 보면 join 다음에 fetch라 되어있는데, 이렇게 하면 연관된 엔티티나 컬렉션을 함께 조회하는데 여기서는 회원(m)과 팀(m.team)을 함께 조회한다. 참고로 일반적인 JPQL 조인과는 다르게 m.team 다음에 별칭이 없는데 페치 조인은 별칭을 사용할 수 없다.

실행된 SQL은 다음과 같다.

SELECT
   M.*, T.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID

페치 조인을 사용하면 그림처럼 SQL 조인을 시도한다.

엔티티 페치 조인 시도

SQL에서 조인의 결과이다.

엔티티 페치 조인 결과 테이블
엔티티 페치 조인 결과 객체

엔티티 페치 조인 JPQL에서 select m 으로 회원 엔티티만 선택했는데 실행된 SQL을 보면 SELECT M.*, T.*로 회원과 연관된 팀도 함께 조회된 것을 확인할 수 있다. 그리고 회원과 팀 객체가 객체 그래프를 유지하면서 조회된 것을 확인할 수 있다. 

이 JPQL을 사용하는 코드다.

String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
   .getResultList();
   
for(Member member : members) {
   //페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 발생 안함
   System.out.println("username = " + member.getUsername() + ", " + 
      "teamname = " + member.getTeam().name());
}

출력 결과는 다음과 같다.

username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B

회원과 팀을 지연 로딩으로 설정했다고 가정해보자. 회원을 조회할 때 페치 조인을 사용해서 팀도 함께 조회했으므로 연관된 팀 엔티티는 프록시가 아닌 실제 엔티티다. 따라서 연관된 팀을 사용해도 지연 로딩이 일어나지 않는다. 그리고 프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트레서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.

 

컬렉션 페치 조인

일대다 관계인 컬렉션을 페치조인 해보겠다.

//컬렉션 페치조인 JPQL
select t
from Team t join fetch t.members
where t.name = '팀A'

팀을 조회하면서 페치조인을 사용해서 연관된 회원의 컬렉션도 함께 조회한다.

//실행된 SQL
SELECT
   T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

컬렉션 페치조인 시도
컬렉션 페치조인 결과조회 테이블
컬렉션 페치조인 결과 객체

컬렉션을 페치조인한 JPQL에서 select t 로 팀만 선택했는데 실행된 SQL을 보면 T.*, M.*로 팀과 연관된 회원도 함께 조회한 것을 확인할 수 있다.

아래는 컬렉션 페치조인 사용 예시이다.

String jpql = "select  t from Team t join fetch t.members where t.name = '팀A'";
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();

for(Team team : teams) {
   
   System.out.println("teamname = " + team.getName() + ", team = " + team);
   
   for(Member member : team.getMembers()) {
   
      //페치조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
      System.out.println(
      "->username = " + member.getUsername() + ", member = " + member);
   }
}

 

출력해보면 같은 '팀A'가 2건 조회된다.(팀A에 회원이 두명 있어서)

 

페치조인과 DISTINCT

SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다. JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거한다. 직전에 컬렉션 페치 조인은 팀A가 중복으로 조회된다.

select distinct t
from Team t join t.members
where t.name = '팀A'

DISTINCT를 사용하면 SQL에 SELECT DISTINCT가 추가된다. 하지만 지금은 각 로우의 데이터가 다르므로 SQL의 DISTINCT는 효과가 없다. 다음처럼 효과가 없다.

로우 번호 회원
1 팀A 회원1
2 팀A 회원2

다음으로 애플리케이션에서 distinct 명령어를 보고 중복된 데이터를 걸러낸다. select distinct t의 의미는 팀 엔티티의 중복을 제거하라는 것이다. 따라서 중복인 팀A는 그림처럼 하나만 조회된다.

페치조인 DISTINCT 결과

 

페치조인과 일반조인의 차이

페치조인을 사용하지 않고 조인만 사용하면 어떻게 될까?

//내부 조인 JPQL
select t
from Team t join t.members m
where t.name = '팀A'
//실행된 SQL
SELECT
   T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀A'

JPQL에서 팀과 회원 컬렉션을 조회했으므로 회원 컬렉션도 함께 조회할 것으로 기대해선 안된다. 실행된 SQL의 SELECT 절을 보면 팀만 조회하고 조인했던 회원은 전혀 조회하지 않는다.

JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다. 만약 회원 컬렉션을 지연 로딩으로 설정하 프록시나 아직 초기화되지 않은 컬렉션 래퍼를 반환한다. 즉시 로딩으로 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행한다.

 

페치조인의 특징과 한계

페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다. 그리고 컬렉션을 지연로딩으로 설정해도 JPQL에서 페치조인을 사용하면 페치 조인을 적용해서 함께 조회한다. 

최적화를 위해 글로벌 로딩 전략(엔티티에 직접 적용하는 로딩 전략)을 즉시 로딩으로 설정하면 애플리케이션 전체에서 항상 즉시 로딩이 일어난다. 물론 일부는 빠를 수는 있지만 전체로 보면 사용하지 않는 엔티티를 자주 로딩하므로 오히려 성능에 악영향을 끼칠 수 있다.따라서 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.

※ 둘 이상의 컬렉션을 페치할 수 없다.
※ 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다. 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어 위험하다.

반응형

'JPA' 카테고리의 다른 글

JPQL - 서브 쿼리  (2) 2023.10.23
JPQL - 경로 표현식  (4) 2023.10.23
JPQL 조인  (2) 2023.10.10
JPQL - (기본 문법, 쿼리 API, 페이징 API, 집합과 정렬)  (0) 2023.09.25
객체지향 쿼리 언어 소개  (0) 2023.09.22