컬렉션을 패치 조인하면 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가한다.
일대다에서 일(1)을 기준으로 페이징 하는 것이 목적이다. 그런데 데이터는 다(N)를 기준으로 row가 생성된다.
Order를 기준으로 페이징 하고 싶은데, 다(N)인 OrderItem을 조인하면 OrderItem이 기준이 되어버린다. 이러한 경우 하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽어서 메모리에서 페이징을 시도한다. 최악의 경우 장애로 이어질 수 있다.
그러면 페이징 + 컬렉션 엔티티를 함께 조회하려면 어떻게 해야할까?
대부분의 페이징 + 컬렉션 엔티티 조회 문제는 이 방법으로 해결할 수 있다.
먼저 ToOne (OneToOne, ManyToOne) 관계를 모두 패치조인한다. 여기서 ToOne 관계는 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.
컬렉션은 지연 로딩으로 조회한다.
지연 로딩 성능 최적화를 위해 hibernate.defalut_batch_fetch_size
, @BatchSize
를 적용한다.
이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size만큼 IN쿼리로 조회한다.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
개별로 설정하고 싶을땐 @BatchSize
를 적용하면 된다. (컬렉션은 컬렉션 필드에, 엔티티는 엔티티 클래스에 적용)
1+N
-> 1+1
로 최적화 된다.참고:
default_batch_fetch_size
의 크기는 적당한 사이즈를 골라야 하는데, 100~1000 사이를 선택하는 것을 권장한다. 이 전략을 SQL IN 절을 사용하는데, 데이터베이스에 따라 IN 절 파라미터를 1000으로 제한하기도 한다. 1000으로 잡으면 한번에 1000개를 DB에서 애플리케이션에 불러오므로 DB 에 순간 부하가 증가할 수 있다. 하지만 애플리케이션은 100이든 1000이든 결국 전체 데이터를 로딩해야 하므로 메모리 사용량이 같다. 1000으로 설정하는 것이 성능상 가장 좋지만, 결국 DB든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는지로 결정하면 된다.
ToOne 관계는 패치 조인해도 페이징에 영향을 주지 않는다. 따라서 ToOne 관계는 패치조인으로 쿼리 수를 줄여 해결하고, 나머지는 hibernate.default_batch_fetch_size
로 최적화 하자.