N+1 문제가 발생하는 컬렉션을 DTO로 조회하는 상황에서 최적화하는 방법을 알아보겠습니다.
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderQueryRepository orderQueryRepository;
@GetMapping("/api/v5/orders")
public List<OrderQueryDto> ordersV5() {
return orderQueryRepository.findAllByDto_optimization();
}
}
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
public List<OrderQueryDto> findAllByDto_optimization() {
List<OrderQueryDto> results = findOrders();
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(results));
results.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return results;
}
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();
}
//...1
private List<Long> toOrderIds(List<OrderQueryDto> results) {
return results.stream()
.map(OrderQueryDto::getOrderId)
.collect(Collectors.toList());
}
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
//...2
List<OrderItemQueryDto> orderItems = 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 in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
//...3
return orderItems.stream()
.collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
}
}
in 절에 넣어줄 orderId 리스트 데이터를 조회된 Order에서 추출합니다.
이전 코드에서는 루프를 돌면서 OrderItem 을 하나씩 가져왔습니다.
하지만 in 절로 한 번에 가져오도록 수정합니다.
조회한 OrderItem 들을 조회한 Order 에 넣어줘야 합니다. 이 때 OrderItem 이 가지고 있는 orderId 와 Order 가 가지고 있는 id 가 동일할 때 넣어줘야합니다.
이 작업을 최적화를 하기 위해서 조회한 OrderItem 들을 key가 orderId 인 Map 으로 변경해서 key 값으로 넣어주면 됩니다.
리스트로 하나씩 뽑아서 매칭하려면 굉장히 불편하겠죠?
이제 총 쿼리는 몇 번 발생할까요?
Query는 루트 1번, 컬렉션 1번 실행됩니다.
ToOne 관계들을 먼저 조회하고, 여기서 얻은 식별자 orderId로 ToMany 관계인 OrderItem 을 in 절로 한꺼번에 조회하기 때문입니다.
그리고 조금 더 성능 최적화를 위해서 MAP을 사용해서 매칭 성능 향상(O(1))시켰습니다.
이전 코드는 N+1 문제가 발생했지만 지금은 컬렉션을 in 절로 한 방에 조회하기 때문에 최적화가 된 것이죠.