반응형

페치 조인(Fetch Join)

  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회한다.
  • 페치 조인은 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면 페치 조인 보다는 일반 조인을 사용해서 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
  • 페치 조인을 사용하면 명시적으로 즉시로딩할 수 있다.
    • 지연로딩 설정이 되어있어도 페치 조인이 우선으로 적용된다.
    • N+1 문제를 해결할 수 있다.
  • 페치 조인 문법
    • [LEFT [OUTER] | INNER] JOIN FETCH 조인경로
  • 예) N:1 페치조인. 회원을 조회하면서 연관된 팀도 함께 조회(회원과 팀은 N : 1 관계)
    //JPQL
    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
  • 예) 1 : N 관계, 컬렉션 페치 조인
    //JPQL
    SELECT t FROM Team t JOIN FETCH t.members
    
    //실제 실행되는 SQL
    SELECT T.*, M.* 
    FROM TEAM T 
    	INNER JOIN MEMBER M 
    	ON T.ID=M.TEAM_ID 
    WHERE T.NAME = '팀A'
    • 팀과 회원(1 : N) 관계에서는 중복된 데이터가 발생한다.
      • 회원1, 회원2 모두 팀A에 소속되어있는 경우, 조인 결과로 팀A가 여러 행으로 나타난다.
        • 팀A 중복 데이터 발생
      • 조회 결과인 팀A는 1차 캐시에 저장되고 Team 인스턴스들은 이를 참조한다.
        • JPA는 동일한 객체에 대해서 유일성을 보장한다. 따라서 팀A의 인스턴스가 여러 개 있어도 이는 동일한 주소를 가진다.
      • 팀A는 자신에게 소속된 회원 정보를 가지고 있다.
    • 중복된 데이터는 DISTINCT 명령어를 통해서 제거할 수 있다.
      • SQL의 DISTINCT는 모든 컬럼의 데이터가 완전히 동일해야 중복으로 인정하기 때문에 중복된 데이터를 제거하는데 한계가 있다.
      • 이를 극복하고자 JPQL의 DISTINCT는 2가지 기능을 제공한다.
        • SQL에 DISTINCT를 추가하는 기능
        • 애플리케이션 레벨에서 엔티티의 중복을 제거하는 기능
          • 같은 식별자를 가진 엔티티를 제거한다.

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

  • JPQL은 결과를 반환할 때 연관관계를 고려하지 않는다.
    • SELECT 절에 지정한 엔티티만 조회한다.
  • 페치 조인 사용 시, 연관된 엔티티를 함께 조회한다.(즉시로딩)
    • 연관된 엔티티에 대한 정보를 얻어오는 동작을 수행해도 조회 SQL이 발생되지 않는다.
  • 일반 조인 사용 시, 연관된 엔티티를 함께 조회하지 않는다.
    • 연관된 엔티티에 대한 정보를 얻어오는 동작을 수행 시, 조회 SQL 발생된다.
//[페치 조인] Team 엔티티와 Member 엔티티 모두 조회한다.
//JPQL
SELECT t FROM Team t JOIN FETCH t.members

//실제 실행되는 SQL
SELECT T.*, M.*
	FROM TEAM T
		INNER JOIN MEMBER M 
		ON T.ID = M.TEAM_ID

//[일반 조인] Team 엔티티만 조회한다.
//JPQL
SELECT t FROM Team t JOIN t.members

//실제 실행되는 SQL
SELECT T.*
	FROM TEAM T
		INNER JOIN MEMBER M 
		ON T.ID = M.TEAM_ID

 

페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    • 하이버네이트는 가능하지만 페치 조인을 여러 개 사용하는 경우를 제외하고는 별칭은 사용하지 않는 것이 좋다.
      • 별칭을 통해서 특정 조건의 데이터만 가져와서 조작하는 것은 위험하다.
      • 특정 조건의 데이터만 조작하는 경우에도 해당되지 않는 나머지 데이터들이 변경되거나 제거될 수 있다. 데이터 정합성 문제 발생
    • JPA의 객체 그래프 탐색의 의도는 연관된 모든 데이터를 가지고 오는 것이다.
    • 특정 조건의 데이터만 필요하다면 별도의 SQL 조회를 하는 것이 좋다.
  • 페치 조인에서 컬렉션은 하나만 지정할 수 있다. 둘 이상의 컬렉션은 페치 조인 할 수 없다.
    • 둘 이상의 컬렉션이 사용된다면 1 : N : N 관계가 되어서 중복된 데이터가 매우 많이 발생한다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
    • 1 : 1, N : 1 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다.
      • 중복 데이터가 발생하지 않는다.
    • 1 : N 과 같은 경우, 메모리에서 페이징하는데 이는 매우 위험하다.
      • 메모리를 모두 사용하여 서버 장애 발생
      • 1 : N의 경우 페이징 방법
        1. N : 1 관계로 SQL을 뒤집어 작성하여 페이징 처리
        1. 페치 조인을 사용하지 않고 필요한 엔티티를 조인한 후, @BatchSize를 이용해서 N+1 문제를 방지하고 페이징 처리
          • @BatchSize는 지연 로딩 시, 연관된 엔티티를 배치사이즈 만큼 IN 쿼리가 생성되어 함께 조회한다.
            • IN 쿼리를 통해서 회원정보도 함께 가져오기 떄문에 추가적인 쿼리가 발생하지 않는다.
            • N+1개의 쿼리가 아니라 사이즈만큼 쿼리 수를 맞출 수 있다.
          • 배치사이즈는 전역 설정으로 설정할 수도 있고 필요한 부분에만 애노테이션으로 설정할 수도 있다.
            • 애노테이션 설정
              • @BatchSize(size = 100)
            • 전역 설정
              • 1000 이하의 값으로 적절하게 설정
              • <property name="default_batch_fetch_size" value="100" />
      • 예) 페이징
      @Entity
      public class Team {
      
          @Id
          @GeneratedValue
          private Long id;
          private String name;
      
          @BatchSize(size = 100)
          @OneToMany(mappedBy = "team")
          private List<Member> members = new ArrayList<>();
      
      }
      
      //1 : N 관계로 하이버네이트에서 경고 발생
      //WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
      List<Team> result = em.createQuery("select t from Team t join fetch t.members", Team.class)
      .setFirstResult(0)
      .setMaxResults(3)
      .getResultList();
      
      //실행된 SQL
      select
                  team0_.id as id1_9_0_,
                  members1_.id as id1_6_1_,
                  team0_.name as name2_9_0_,
                  members1_.age as age2_6_1_,
                  members1_.USERNAME as USERNAME6_6_1_,
                  members1_.TEAM_ID as TEAM_ID10_6_1_,
                  members1_.TEAM_ID as TEAM_ID10_6_0__,
                  members1_.id as id1_6_0__ 
              from
                  Team team0_ 
              inner join
                  Member members1_ 
                      on team0_.id=members1_.TEAM_ID
      
      //N : 1 관계로 SQL 작성하여 페이징 처리
      List<Member> result = em.createQuery("select m from Member m join fetch m.team", Member.class)
      .setFirstResult(0)
      .setMaxResults(3)
      .getResultList();
      
      //실행된 SQL
      select
                  member0_.id as id1_6_0_,
                  team1_.id as id1_9_1_,
                  member0_.age as age2_6_0_,
                  member0_.USERNAME as USERNAME6_6_0_,
                  member0_.TEAM_ID as TEAM_ID10_6_0_,
                  team1_.name as name2_9_1_ 
              from
                  Member member0_ 
              inner join
                  Team team1_ 
                      on member0_.TEAM_ID=team1_.id limit ?
      
      //@BatchSize를 이용해서 페이징 처리
      List<Team> result = em.createQuery("select t from Team t", Team.class)
      .setFirstResult(0)
      .setMaxResults(3) // 3건 IN 쿼리 발생. (130건 까지 가져온다면 100건 IN 쿼리 발생.)
      .getResultList();
      
      //실행된 SQL
      select
          members0_.TEAM_ID as TEAM_ID10_6_1_,
          members0_.id as id1_6_1_,
          members0_.id as id1_6_0_,
          members0_.age as age2_6_0_,
          members0_.USERNAME as USERNAME6_6_0_,
          members0_.TEAM_ID as TEAM_ID10_6_0_,
      from
          Member members0_ 
      where
          members0_.TEAM_ID in ( // 3건 IN 쿼리 발생
              ?, ?, ?
          )

 


[참고자료]

자바 ORM 표준 JPA 프로그래밍 - 기본편, 김영한

반응형

+ Recent posts