엔티티를 DTO로 변환했지만 N+1 문제를 만났습니다.

예를 들어, 주문이 10개면 최악의 경우에 처음에 주문 가져오는 쿼리 1번이 나가고 그 다음에 회원을 레이지로딩 10번을 해야 되고 배송을 레이지로딩 10번을 해야 되니까 총 21번의 쿼리가 나가는 성능상의 문제를 봤습니다.

상식적으로 생각해도 네크워크를 너무 많이 왔다 갔다 하는 것 같죠?

이런 경우에 성능 최적화를 어떻게 하는지 알아보겠습니다.

간단한 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    /**
     * V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용O)
     * - fetch join으로 쿼리 1번 호출
     */
    @GetMapping("/api/v3/simple-orders")
    public List<SimpleOrderDto> ordersV3() {
        List<Order> orders = orderRepository.findAllWithMemberDelivery();
        return orders.stream()
                .map(SimpleOrderDto::new)
                .collect(Collectors.toList());
    }

    @Data
    static class SimpleOrderDto {
        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;

        public SimpleOrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
        }
    }

}

@Repository
@RequiredArgsConstructor
public class OrderRepository {

    private final EntityManager em;

    public List<Order> findAllWithMemberDelivery() {
        return em.createQuery(
                        "select o from Order o" +
                                " join fetch o.member m" +
                                " join fetch o.delivery d", Order.class)
                .getResultList();
    }

}

Order 를 Member, Delivery 와 조인해서 한 방에 다 땡겨오는 겁니다.

즉, 엔티티를 페치 조인(fetch join)을 사용해서 쿼리 1번에 조회하는 겁니다. 페치 조인을 하면 지연로딩을 무시하고 진짜 객체 값을 채워서 가져옵니다.

참고로, fetch 는 SQL 문법은 아니고 jpa에만 있는 문법입니다.

페치 조인 자세히

<aside> ❗ 여담으로 실무에서는 객체 그래프가 자주 사용하는게 보통 정해져 있습니다.

예를 들어, “주문을 만들 때 회원과 배달 정보를 같이 쓴다” 는 식으로요.

그래서 findAllWithMemberDelivery() 처럼 메서드를 길게 만들었는데 객체 그래프가 자주 사용하는 걸로 정해져 있다면 findAll() 처럼 메서드를 간단하게 만들기도 합니다.

</aside>

바로 실행을 해서 쿼리를 살펴봅시다.

Untitled