일반 Join과 Fetch Join

일반 Join

일반 조인을 하는 경우 조회의 주체가 되는 엔티티만 불러온다.
만약 조인된 엔티티의 다른 값을 조회하게 된다면 아래처럼 select 쿼리를 호출하게된다.

    select o1_0.order_id, o1_0.delivery_id, o1_0.member_id, o1_0.order_date, o1_0.status 
    from orders o1_0 
    join member m1_0 on m1_0.member_id=o1_0.member_id 
    join delivery d1_0 on d1_0.delivery_id=o1_0.delivery_id

    select m1_0.member_id, m1_0.city, m1_0.street, m1_0.zipcode, m1_0.name 
    from member m1_0 
    where m1_0.member_id=?

    select d1_0.delivery_id, d1_0.city, d1_0.street, d1_0.zipcode, d1_0.delivery_status 
    from delivery d1_0 
    where d1_0.delivery_id=?

    select o1_0.order_id, o1_0.delivery_id, o1_0.member_id, o1_0.order_date, o1_0.status 
    from orders o1_0 
    where o1_0.delivery_id=?

    select m1_0.member_id, m1_0.city, m1_0.street, m1_0.zipcode, m1_0.name 
    from member m1_0 
    where m1_0.member_id=?

    select d1_0.delivery_id, d1_0.city, d1_0.street, d1_0.zipcode, d1_0.delivery_status 
    from delivery d1_0 
    where d1_0.delivery_id=?

    select o1_0.order_id, o1_0.delivery_id, o1_0.member_id, o1_0.order_date, o1_0.status 
    from orders o1_0 
    where o1_0.delivery_id=?

이렇게 되면 DB와 커넥션을 계속 주고 받아야 하므로 성능에 좋지않다.

fetch join

이러한 이유로 최적화를 하기위해 fetch join을 사용한다.
fetch join을 사용하게 되면 쿼리를 한번만 호출해도 된다.

select o1_0.order_id, d1_0.delivery_id, d1_0.city, d1_0.street, d1_0.zipcode, d1_0.delivery_status,
        m1_0.member_id, m1_0.city, m1_0.street, m1_0.zipcode, m1_0.name,
        o1_0.order_date, o1_0.status 
    from orders o1_0 
    join member m1_0  on m1_0.member_id=o1_0.member_id 
    join delivery d1_0  on d1_0.delivery_id=o1_0.delivery_id

페치조인 주의점

컬렉션은 페치 조인하면 페이징이 불가능 하다.  
1대 N이기 때문에 데이터가 예측할 수 없이 증가하게 된다.  
하지만 이 상태에서 페이징을 하게 되면 N을 기준으로 페이징을 하게되는데
그 결과는 원하는 결과가 아니게 된다.  
distinct를 하여 중복 데이터를 줄이고 결과를 낼 수 도 있다.  
하지만 쿼리 결과를 확인하면 전체 조회를 하게되며
하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽어서 메모리에서 페이징을 시도한다.  
out of memory가 발생할 수 있다.

해결방법

대부분의 페이징 + 컬렉션 엔티티 조회 문제는 아래 방법으로 해결이 가능하다.
~ToOne 관계는 모두 fetch join을 한다. ~ToOne관계는 row 수를 증가 시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.

해결방법: default_batch_fetch_size를 default로 걸어두는게 좋다.
default_batch_fetch_size 또는 @BatchSize를 조절한다.

  • default_batch_fetch_size : 글로벌 설정 100~1000 사이로 조절하는게 가장 좋다.(데이터베이스의 스펙에 의해 갈린다.)
  • @BatchSize 개별 설정

default_batch_fetch_size를 설정하게 되면 in 쿼리로 변경되게 된다.
쿼리 호출수가 N+1에서 1+1로 최적화 된다.
결론:ToOne관계는 fetch join으로 쿼리수를 줄이고 나머지는 batch fetch size로 최적화 하자

권장 순서

  1. 엔티티 조회 방식으로 우선 접근
    1. 페치 조인으로 쿼리 수를 최적화
    2. 컬렉션 최적화
      1. 페이징 필요시 default_batch_fetch_size, @BatchSize로 최적화
      2. 페이징 필요X -> 페치 조인 사용
  2. 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
  3. DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate
참고: 엔티티 조회 방식은 페치 조인이나, default_batch_fetch_size, @BatchSize
같이 코드를 거의 수정하지 않고, 옵션만 약간 변경해서, 다양한 성능 최적화를 시도할 수 있다.
반면에 DTO를 직접 조회하는 방식은 성능을 최적화 하거나 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다.