컬렉션을 페치조인시 일대다 조인이 발생하여 데이터 뻥튀기가 발생하며 예측할 수 없이 쿼리가 증가하게 되는걸 경험할 수 있다.
일대다에서 일(1) 기준으로 페이징을 하는 것을 목적으로 하지만, 데이터는 다(N)기준으로 row가 생성된다는 불일치성이 발생한다. Order 기준으로 페이징하고 싶은데 OrderItem으로 조인하면 OrderItem이 기준이 되어버리기 때문이다.
🫸이 경우, 모든 DB를 읽어서 메모리에 올려두고 메모리에서 페이징을 시도하므로 최악은 장애까지 이어질 수 있다는 우려가 있다.
ToOne 관계는 row 수를 증가시키지 않으므로 페이징 쿼리에 영향을 미치지 않기 때문이다.
컬렉션은 지연로딩으로 조회한다.(페치조인 X)
지연로딩 성능 최적화를 위해서 hibernate.default_batch_fetch_size
(전역설정) 또는 @BatchSize
(개별 설정)을 적용한다.
이 옵션을 통해 조회한 엔티티에 관련된 컬렉션이나 프록시 객체들을 한꺼번에 설정한 size만큼 IN쿼리로 조회할 수 있다.
이 경우 컬렉션 조회로, 지연로딩을 해야하는데 먼저 Order이 조회될때 OrderItem은 batch size로 설정된 값만큼 모두 로딩되어진다.
이 경우 컬렉션 조회는 아니지만, 지연로딩을 해야하는데 먼저 Order이 조회될 때 프록시 객체로 설정되고 batch size로 설정된 값만큼 모두 로딩되어 진다.
Repository
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
api controller
/**
* V3.1 엔티티를 조회해서 DTO로 변환 페이징 고려
* - ToOne 관계만 우선 모두 페치 조인으로 최적화
* - 컬렉션 관계는 hibernate.default_batch_fetch_size, @BatchSize로 최적화
*/
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page(@RequestParam(value = "offset",
defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue
= "100") int limit) {
List<Order> orders = orderRepository.findAllWithMemberDelivery(offset,
limit);
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
최적화 옵션 설정(전역설정 방법에 해당)
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
ToOne 관계는 페치조인을 해도 페이징에 영향을 주지 않는다. 따라서 이 관계는 페치조인으로 쿼리수를 줄이고 나머지는 전역설정이나 개별설정을 통해 배치 사이즈를 설정해 컬렉션, 프록시 객체들 조회를 최적화하도록 하자.