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