코딩과 결혼합니다

[JPA] 컬렉션 조회 최적화(1) 본문

2세/JPA

[JPA] 컬렉션 조회 최적화(1)

코딩러버 2024. 2. 25. 17:42
728x90
 이전에는 xToOne일 때의 조회 최적화를 해보았는데 이번에는 xToMany일 때 (컬렉션 조회) 최적화를 하는 방법에 대해서 정리하고자 한다.
 DB 입장에서 1대 N 조인을 하면 조회하는 순간 데이터 결과가 N개만큼 커지게 된다. xToOne일 때에는 이 부분을 고려하지 않아도 되지만, 이러한 경우에는 최적화할 때 좀 더 고민해야 할 포인트가 많아진다.

 

@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
    List<Order> all = orderRepository.findAllByString(new OrderSearch());
    for (Order order : all) {
        order.getMember().getName();
        order.getDelivery().getAddress();

        List<OrderItem> orderItems = order.getOrderItems();
        orderItems.stream().forEach(o -> o.getItem().getName());
    }
    return all;
}

==========================================================================

//OrderItem Entity의 필드中
//양방향 관계 문제를 해결하기 위해 한 쪽에 @JsonIgnore
@JsonIgnore
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "order_id")
private Order order;

Entity를 직접 노출하는 방법이다. Hibernate6 Module을 등록하고, LAZY 강제 초기화를 하여 필요한 데이터만을 뽑아온다.

(이전에 글로 적었지만 이렇게 초기화하지 않으면 모든 데이터들을 다 끌고 오게 된다.  Hibernate6Module은 초기화된 데이터들만을 가져올 수 있게 동작한다.)

 

주문과 관련된 ORderItems를 다 가져와 또 이를 돌리며 Item 정보를 가져온다. Item은 이름만 필요로 하기 때문에 이것도 o.getItem().getName()과 같이 초기화를 해주었다.

 

➡️이 방법은 엔티티를 직접 노출하는 데에서 생기는 많은 문제들이 발생한다.

https://coding-s2-chaewon.tistory.com/252


@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
    List<Order> orders = orderRepository.findAllByString(new OrderSearch());
    List<OrderDto> collect = orders.stream()
            .map(o -> new OrderDto(o))
            .collect(Collectors.toList());

    return collect;

}

Entity를 DTO로 변환하는 방법이다. 

@Getter
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.getStatus();
        address = order.getDelivery().getAddress();
        orderItems = order.getOrderItems().stream()
                .map(orderItem -> new OrderItemDto(orderItem))
                .collect(Collectors.toList());
    }
}

여기서 주의할 점은 Order 안에 OrderItems가 있는데 이것도 Entity를 그대로 반환하면 안 되고 Dto로 변환해야 한다는 것이다. 마찬가지로 OrderItemDto를 만들어 변환해 주었다. 엔티티에 대한 의존을 완전히 끊어야 한다. 

@Getter
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();
    }
}

또한 프록시를 초기화하지 않고 돌리게 되면 orderItems의 값이 null이 나오게 되므로 필요한 데이터를 초기화해 주었다.

 

➡️ 이 방법은 쿼리가 굉장히 많이 나가게 된다. (영속성 컨텍스트에 있는 엔티티를 사용한다면 SQL을 실행하지 않는다.)

  1. order 1번
  2. member, address N번(order 조회 수만큼) 
  3. orderItem N번(order 조회 수만큼) 
  4. itemN번(orderItem 조회 수만큼)