저번 시간에 이어서 조인 중이 페치 조인에 대해서 알아보자.
저번에는 내부 외부 컬렉션 세타 조인에 대해서 알아봤다.
페치 조인은 우리가 흔히 쓰던 sql 조인이 아니고 JPQL에서 성능 최적화를 위해 제공되는 기능이다. 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능으로 fetch 명령어를 사용할 수 있다.

페치 조인 ::= [LEFT [OUTER] | INNER] JOIN FETCH 조인경로

위는 JPA 표준 명세에 정의된 페치 조인이다.

엔티티 페치 조인

페치 조인을 사용해 보자

select m
from Member m join fetch m.team

연관된 엔티티와 컬렉션을 함께 조회하는데 여기서 회원(m)과 팀(m.team)을 함께 조회한다. 일반 JPQL과는 다르게 m.team 에는 별칭이 없는데 페치 조인에서는 별칭을 사용할 수 없다.

예제를 보자

  private static void fetchJoin(EntityManager entityManager) {
    String jpql = "select m from Member m join fetch m.team";
    List<Member> members = entityManager.createQuery(jpql, Member.class)
      .getResultList();

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

만약 회원과 팀을 지연로딩으로 설정했다고 가정하자. 회원을 조회할 때는 페치조인을 사용해서 팀도 함께 조회 했으므로 연관된 팀 엔티티는 프록시가 아닌 실제 엔티티를 가져온다. 이때 만약 준영속상태가 되어도 연관된 팀 엔티티를 조회 할 수 있다.

컬렉션 페치 조인

이번엔 위와 동일하지만 일대다 관계인 컬렉션을 페치 조인 해보자.

select t
from Team t join fetch t.members
from t.name = 'team1'

이렇게 페치 조인을 하면 연관된 member 컬렉션도 함께 조회된다.

  private static void collectionFetchJoin(EntityManager entityManager) {
    String jpql = "select t from Team t join fetch t.members where t.name = 'team1'";
    List<Team> teams = entityManager.createQuery(jpql, Team.class)
      .getResultList();

    for(Team team : teams){
      System.out.println("teamname = " + team.getName());

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

이거 역시 페치 조인으로 지연로딩이 발생 하지 않는다.
하지만 여기서 문제는 팀은 하나지만 member 테이블과 조인하면서 결과가 증가했다. 그래서 team 엔티티에 2건이 조회 되었다.

페치 조인과 DISTINCT

SQL의 DISTINCT는 중복된 결과를 제거 하는 명령어다. JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가 하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거한다.

select distinct t
from Team t join fetch t.members
where t.name = 'team1'

select distinct t 의 의미는 팀의 엔티티의 중복을 제거하는 것이다. 따라서 중복으로 조회 되었던 team1이 한건만 조회된다.

  private static void collectionFetchJoinDistinct(EntityManager entityManager) {
    String jpql = "select distinct t from Team t join fetch t.members where t.name = 'team1'";
    List<Team> teams = entityManager.createQuery(jpql, Team.class)
      .getResultList();

    for(Team team : teams){
      System.out.println("teamname = " + team.getName());

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

페치 조인의 특징과 한계

페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 준다.
글로벌 전략을 지연로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인을 적용해서 함께 사용한다.

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

페치 조인의 한계
1. 페치 조인 대상에는 별칭을 줄 수 없다.
2. 둘 이상의 컬렉션을 페치 할 수 없다.
3. 컬렉션을 페치 조인하면 페이징 API를 사용 할 수 없다.

물론 위에 조건은 구현체마다 조금씩 다르다.

이렇게 페치 조인에 대해서 조금 알아봤다.

출처 : 자바 ORM 표준 JPA 프로그래밍 (김영한)