엔티티를 DTO로 변환했지만 N+1 문제를 만났습니다.
예를 들어, 주문이 10개면 최악의 경우에 처음에 주문 가져오는 쿼리 1번이 나가고 그 다음에 회원을 레이지로딩 10번을 해야 되고 배송을 레이지로딩 10번을 해야 되니까 총 21번의 쿼리가 나가는 성능상의 문제를 봤습니다.
상식적으로 생각해도 네크워크를 너무 많이 왔다 갔다 하는 것 같죠?
이런 경우에 성능 최적화를 어떻게 하는지 알아보겠습니다.
@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>
바로 실행을 해서 쿼리를 살펴봅시다.