DEV-STUDY/Spring

[JPA] 컬렉션 LAZY Loading시 페이징 기법

HwangJerry 2023. 6. 27. 23:36

엔티티를 조회할 때, 컬렉션 필드의 경우 LAZY Loading을 피하겠다는 의도로 JOIN FETCH를 하면 데이터가 뻥튀기되는 문제가 발생한다.

 

또한 페이징을 수행하는 것이 (거의) 불가능하다는 것이 문제였다.

 

이를 해결하기 위해서는 LAZY Loading을 그냥 적용하되, 한 번에 일정 사이즈를 fetch하는 것(==batch)으로 하면 해결된다. 단, XToOne 관계 필드는 JOIN FETCH하여도 문제가 발생하지 않으므로, 가급적 XToOne 필드에 대해서는 JOIN FETCH를 적용하는 것을 적극 권장한다.

/**
 * 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;
}
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();
}

 

Batch Size 설정하기

application level에서 적용하려면 application.yml에 적용하면 된다.

default_batch_fetch_size: 1000 #최적화 옵션 - 글로벌하게 적용

 

조회하는 엔티티 내의 특정 컬렉션 필드에 별도 사이즈를 적용하려면 해당 필드에 @BatchSize 어노테이션을 적용하면 된다.

@BatchSize(size = 1000)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();

 

만약 컬렉션 필드가 가지고 있는 연관관계 필드 중에서 특정 XToOne 필드에 대해 배치 사이즈를 조정하고 싶다면 해당 필드의 클래스 레벨에 @BatchSize 어노테이션을 적용해야 한다.

@BatchSize(size = 100)
@Entity
...
public abstract class Item {

    @Id
    @GeneratedValue
    @Column(name = "item_id")
    private Long id;
    
    ...
}

 

주의

일반적으로 batch size는 100 ~ 1000 정도로 잡는 것을 권장하고 있다. batch size는 SQL IN 절을 사용하여 구현되는데, 이 때문에 DB에 따라서 IN절 파라미터를 1000으로 제한하기도 하여 배치 사이즈가 1000이 넘어가면 에러를 일으키는 경우도 발생할 수 있으므로 최대 1000정도까지만 사이즈를 잡는 것이 좋다.

 

또한 배치 사이즈를 1000으로 잡으면 DB와 어플리케이션에 그 만큼의 부하가 순간적으로 가해진다는 것을 유념해야 한다. 따라서 WAS와 DB가 순간 부하를 견딜 수 있다면 성능상으론 1000이 가장 좋은 퍼포먼스를 보여줄 것이다. 100이든 1000이든 결국은 목표하는 동일한 양의 데이터를 불러와야 하므로 메모리 사용량은 동일하므로, WAS와 DB가 순간 부하를 얼마나 버틸 수 있는지를 감안하여 사이즈를 선정하면 된다.