ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 쿼리 N+1 상황을 해결하는 방법
    Spring 2021. 11. 23. 01:59

    엔티티를 DTO로 변환하여 셀렉을 하는 경우 N+1번 쿼리가 발생하는 상황이 종종온다.

    order엔티티에 member엔티티와 delivery엔티티가 존재할 때, order테이블을 조회하면,

     

    - order 조회 1번(order 조회 결과 수 N이 된다.)

    - order -> member (지연 로딩 조회 N번)

    - order -> delivery (지연 로딩 조회 N번)

     

    총 2N+1번 쿼리가 발생을 한다.

    이는 성능상에 큰 문제를 야기할 수 있고, 성능최적화를 해야만 한다. 이 상황을 해결하기 위한 2가지 방법이 있다.

     

     

    엔티티를 DTO로 변환 - 페치 조인 최적화

    Controller

    @GetMapping("/api/v3/simple-orders")
        public List<SimpleOrderDto> ordersV3() {
    
            List<Order> orders = orderRepository.findAllWithMemberDelivery();
            List<SimpleOrderDto> result = orders.stream()
                    .map(o -> new SimpleOrderDto(o))
                    .collect(Collectors.toList());
    
            return result;
        }

    Repository

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

    엔티티를 페치 조인(fetch join)을 사용해서 쿼리 1번에 조회

    페치 조인으로 order -> member, order -> delivery는 이미 조회된 상태이므로 지연로딩x

     

    결과 쿼리

    JPA에서 DTO로 바로 조회

    DTO

    @Data
    public class OrderSimpleQueryDto {
    
        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;
    
        public OrderSimpleQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
            this.orderId = orderId;
            this.name = name;
            this.orderDate = orderDate;
            this.orderStatus = orderStatus;
            this.address = address;
        }
    }

    Repository

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

    Controller

    @GetMapping("/api/v4/simple-orders")
        public List<OrderSimpleQueryDto> ordersV4() {
    
            List<OrderSimpleQueryDto> orders = orderSimpleQueryRepository.findOrdersDtos();
    
            return orders;
        }

    일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회

    new명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환

    SELECT절에서 원하는 데이터를 직접 선택하므로 DB -> 애플리케이션 네트웍 용량 최적화(생각보다 미비)

    리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

     

    정리

    엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법은 각각 장단점이 있다. 둘중 상황에 따라서 더 나은 방법을 선택하면 된다. 엔티티로 조회하면 리포지토리 재사용성도 좋고, 개발도 단순해진다.

    따라서 권장하는 방법은 다음과 같다.

     

    쿼리 방식 선택 권장 순서

    1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.

    2. 필요하면 페치 조인으로 성능을 최적화 한다. -> 대부분의 성능 이슈가 해결된다.

    3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.

    4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.

     

     

     

    출처 : 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 - 인프런 | 학습 페이지 (inflearn.com)

    댓글

Designed by Tistory.