본문 바로가기

CS 잡지식

(1 + N) 문제 해결책 - (Feat. Fetch Join, @BatchSize, batch_fetch_size 설정)

Lazy 객체에서 생기는 (1+N) 문제의 대부분은 Fetch Join이라는 강력한 옵션으로 해결이 가능하다.

그러나 아래 사이트의 맥락을 이어서 부연 설명을 하자면, 多에 해당하는 컬렉션에 대해서 Fetch Join을 하게 되면

여러 가지 이슈가 생긴다(아래 사이트에도 나와 있지만, [페이징] 이슈 등)

https://jbluke.tistory.com/427

 

[1:다]에서의 컬렉션(다) 조회 시 문제 : 페이징, (N+1)문제 등

https://jbluke.tistory.com/426 ( 이 사이트의 맥락에 이어서 설명을 하겠다) 컬렉션(Collection) fetch join 시, 발생하는 [데이터 중복] 문제!(소위 [데이터 뻥튀기]라고도 부름) [1:다]에서 생기는 문제이기도

jbluke.tistory.com

그래서, 多에 해당하는 컬렉션에 대한 최적화는 Fetch Join으로 하면 안된다.

Lazy가 걸린 객체(OrderItem,Item)을 Fetch Join을 사용하지 않고 최적화하는 유일한 방법

1] hibernate.default_batch_fetch_size: [글로벌] 설정

2] @BatchSize: [개별] 최적화(특정 엔티티에 설정할 때 사용)

-> 이 옵션을 사용하면 컬렉션(OrderItem)이나, 프록시 객체(OrderItem, Item)를 한꺼번에 [설정한 size 만큼] IN 쿼리로

1번의 SQL문으로 조회한다.

( 보통, (1 + N) 문제가 터지면, 1개씩 SQL문을 N번 만큼 날려서 조회해 온다. 

batch_size를 설정을 해 놓으면, fetch join이 아니다라도, 설정한 사이즈 만큼 조회를 해 온다.)

public List<Order> findAllWithMemberDelivery() {

    //Fetch Join 사용하여 1번의 SQL문으로 [Member + Deliver] 조회
    List<Order> resultList = entityManager.createQuery(
                    "select o from Order o " +

                            "join fetch " +
                            //@~ToOne : 무조건 fetch join으로 최적화
                            "o.member m " +

                            "join fetch " +
                            //@~ToOne
                            "o.delivery d"

                    , Order.class)
            .getResultList();

    return resultList;
}
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_1(
        @RequestParam(value ="offset",defaultValue = "0") int offset,
        @RequestParam(value= "limit",defaultValue = "100") int limit)
{
    // 페이징 기능을 구현하기 위해, findAllWithMemberDelivery()를 오버로딩 했음.
    List<Order> orders = orderRepository.findAllWithMemberDelivery(offset,limit);

    // OrderDto 클래스 내에서 Lazy가 걸린 @ToMany 엔티티(컬렉션)에 대해서
    // (N + 1) 문제를 일으키지 않고, IN 쿼리로 조회를 1번의 SQL로 함
   List<OrderDto> result = orders.stream()
            .map(o->new OrderDto(o)) // new OrderDto() 생성자 내에서 OrderItem 컬렉션이 IN 쿼리를
            		                    // 이용하여, 1번 SQL문으로 fetch_size만큼 
                                        [페이징] 조회가 되었다. 
            .collect(Collectors.toList());

    return result;

}

 

@ToOne 객체를 젤 먼저 조회를 한다. 그런데 API 호출시 세팅한 범위만큼

페이징을 적용해서 조회를 해온다. 

@Data
static class OrderDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate; //주문시간
    private OrderStatus orderStatus;
    private Address address;

    private List<OrderItemDto> orderItems;
    public OrderDto(Order order) {

        orderId = order.getId();

        name = order.getMember().getName();

        orderDate = order.getOrderDate();

        orderStatus = order.getOrderStatus();

        address = order.getDelivery().getAddress();

      // 여기가 실행될 때, new OrderItemDto(orderItem) 생성자 내에서 Proxy 객체인 Item 을 
      //IN 쿼리를 이용하여, 1번 SQL문으로 fetch_size만큼
      [페이징] 조회가 되었다.
        orderItems = order.getOrderItems().stream()
                .map(orderItem -> new OrderItemDto(orderItem))
                .collect(toList());
    }
}

2개의 ?가 있다. 

이는 2개의 order_id, 즉 2개의 상품에 대한 OrderItem를 조회하겠다는 뜻이다. 

(어마어마한 최적화이다. 만약 batch_fectch_size를 등록해주지 않았다면, Lazy 로딩에 의해서, (1+N)문제를 일으켰을 것이다)

@Data
static class OrderItemDto {
    private String itemName;//상품 명
    private int orderPrice; //주문 가격
    private int count; //주문 수량
    public OrderItemDto(OrderItem orderItem) {

        itemName = orderItem.getItem().getName();  
                                                   
        orderPrice = orderItem.getOrderPrice();

        count = orderItem.getCount();
    }
}

4개의 ?가 있다. 

이는 4개의 item_id에 해당하는 ITEM 객체를 조회하겠다는 뜻이다.

1번의 SQL문으로 4개의 ITEM을 모~~두 조회해왔다

(어마 어마한 최적화이다. bach_fetch_size를 등록해주지 않았다면, (1+N)에 의해 총 4번의 SQL문이 호출되었을 것이다)

(지금 위 쿼리는 batch_fetch_size를 100으로 설정을 하였는데, 만약 Item 객체가 1000개 있으면, 10번의 roop가 돌면서

총 10번의 SQL문으로 1000개의 SQL문으로 조회하는 어마어마한 최적화이다. 

이런 최적화는 개발자가 직접 하기는 매우 어렵다)

fetch_size는 조회되는 객체의 개수가 대략 어느 정도인지 파악을 하여 최소한의 SQL문으로, 즉 최소한의 ROOP문으로

데이터를 조회해 올 수 있게 개발자가 직접 설정을 해주자.

아래 사이트에서 배치 사이즈 팁을 확인하라

https://jbluke.tistory.com/429

현재 Lazy가 걸려 있는 OrderItem 컬렉션에 대해서, Controller에서서 얻은 [offset,limit]로부터 

Order 객체에 대한 1개의 OrderItem 컬렉션 내에서 offset = 1, limit =100을 고려하여

100개 만큼만 들고 오라고 , 

Fetch join으로, 조회되지 않는 OrderItem에 대해서 [페이징] 설정을 해준 것이다. 

-> 이로 인해 Lazy이면서, fetch join으로 조회되지 않는 컬렉션을 아래와 같이 IN 쿼리 문으로 단 1번의 SQL문으로

[fetch_size만큼]데이터를 들고 올 수가 있다. ( SQL문은 맨 아래에서 확인을 할 수가 있다)

결론 : [1:다] 관계에서 컬렉션(OrderItem)를 JOIN(Fetch join 포함)하면서 생기는 [페이징] 문제를 batch_fetch_size와 In

쿼리를용하여 해결하였다. 

 

(이 게시물은 코드를 보면서 꼭 읽어 봐야 한다.)