컬렉션 조회 최적화 - JPA에서 DTO 직접 조회

HotFried·2023년 11월 27일
0

Tips !!

  • 화면이나 API 용도로만 사용하는 쿼리는 패키지를 따로 파서 구현한다.
  • 화면과 관련된 로직과 중요 핵심 비즈니스 로직을 분리할 수 있다.

Code

OrderApiController

@RestController
@RequiredArgsConstructor
public class OrderApiController {

    private final OrderRepository orderRepository;
    private final OrderQueryRepository orderQueryRepository;

    @GetMapping("/api/v4/orders")
    public List<OrderQueryDto> ordersV4() {
        return orderQueryRepository.findOrderQueryDtos();
    }
}

Get 방식으로 메소드를 생성한다.
DTO로 직접 조회를 할 것이기 때문에, OrderQueryRepository를 만든 뒤 findOrderQueryDtos() 메소드를 구현한다.


OrderQueryRepository

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    public List<OrderQueryDto> findOrderQueryDtos() {
        List<OrderQueryDto> result = findOrders();

        result.forEach(o -> {
            List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
            o.setOrderItems(orderItems);
        });
        return result;
    }

    private List<OrderQueryDto> findOrders() {
        return em.createQuery(
                        "select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name,o.orderDate,o.status,d.address)"
                                + " from Order o" +
                                " join o.member m" +
                                " join o.delivery d", OrderQueryDto.class)
                .getResultList();
    }

    private List<OrderItemQueryDto> findOrderItems(Long orderId) {
        return em.createQuery(
                        "select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)"
                                +
                                " from OrderItem oi" +
                                " join oi.item i" +
                                " where oi.order.id = :orderId", OrderItemQueryDto.class)
                .setParameter("orderId", orderId)
                .getResultList();
    }
}

아래는 OrderQueryDto의 코드이다.

@Data
public class OrderQueryDto {

    private Long orderId;
    private String name;
    private LocalDateTime localDateTime;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemQueryDto> orderItems;

    public OrderQueryDto(Long orderId, String name, LocalDateTime localDateTime, OrderStatus orderStatus,
                         Address address) {
        this.orderId = orderId;
        this.name = name;
        this.localDateTime = localDateTime;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}

Dto를 살펴보면 orderItems 컬렉션은 생성자에서 제외를 한다.
OrderQueryRepositoryfindOrderQueryDtos()를 살펴보면, 컬렉션을 별도의 Dto를 통해 가져온 뒤 OrderQueryDto에 Setter로 추가해주는 방식을 택하고 있기 때문이다.

정리

  • 즉, ToOne 관계는 join을 해도 데이터의 row 수가 증가 하지 않기 때문에
    최적화하기 쉽기 때문에 한 번에 조회한다.

  • ToMany 관계는 join시 데이터의 row 수가 증가하기 때문에 최적화가 어렵다.

  • 따라서, ToOne관계를 먼저 조회하고 ToMany 관계는 별도로 처리한다.

아래는 OrderItemQueryDto이다.

@Data
public class OrderItemQueryDto {
    
    @JsonIgnore
    private Long orderId;
    private String itemName;
    private int orderPrice;
    private int count;


    public OrderItemQueryDto(Long orderId, String itemName, int orderPrice, int count) {
        this.orderId = orderId;
        this.itemName = itemName;
        this.orderPrice = orderPrice;
        this.count = count;
    }
}

쿼리는 몇번 나갔을까?

우리가 찾을 정보는 Order1개당 2개의 OrderItem이 있고,
Order는 2개가 존재한다.

즉 Order 2개 - OrderItem 4개인 셈이다.

    select
        order0_.order_id as col_0_0_,
        member1_.name as col_1_0_,
        order0_.order_date as col_2_0_,
        order0_.status as col_3_0_,
        delivery2_.city as col_4_0_,
        delivery2_.street as col_4_1_,
        delivery2_.zipcode as col_4_2_ 
    from
        orders order0_ 
    inner join
        member member1_ 
            on order0_.member_id=member1_.member_id 
    inner join
        delivery delivery2_ 
            on order0_.delivery_id=delivery2_.delivery_id

--------------------------------------------------------------

    select
        orderitem0_.order_id as col_0_0_,
        item1_.name as col_1_0_,
        orderitem0_.order_price as col_2_0_,
        orderitem0_.count as col_3_0_ 
    from
        order_item orderitem0_ 
    inner join
        item item1_ 
            on orderitem0_.item_id=item1_.item_id 
    where
        orderitem0_.order_id=?
        
--------------------------------------------------------------

    select
        orderitem0_.order_id as col_0_0_,
        item1_.name as col_1_0_,
        orderitem0_.order_price as col_2_0_,
        orderitem0_.count as col_3_0_ 
    from
        order_item orderitem0_ 
    inner join
        item item1_ 
            on orderitem0_.item_id=item1_.item_id 
    where
        orderitem0_.order_id=?

위 쿼리를 보면 Order를 조회할 때 1번, 각각의 OrderItem을 조회할 때 2번
총 3번의 쿼리가 발생한다.


문제점

public class OrderQueryRepository {

    private final EntityManager em;

    public List<OrderQueryDto> findOrderQueryDtos() {
        // query 1번 -> 결과는 N개
        List<OrderQueryDto> result = findOrders();

        // query N번
        result.forEach(o -> {
            List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
            o.setOrderItems(orderItems);
        });
        return result;
    }
}

findOrders() 메소드를 통해 쿼리를 1번 날렸을 때 N번의 쿼리가 추가로 발생해서 N+1 문제가 발생하기 때문에 최적화가 필요해 보인다.


참고 :

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

profile
꾸준하게

0개의 댓글