nu_s

[JPA 활용] 조회 성능 최적화 - 엔티티를 DTO로 변환(페치 조인) 🐱 본문

JPA

[JPA 활용] 조회 성능 최적화 - 엔티티를 DTO로 변환(페치 조인) 🐱

woochii 2023. 11. 8. 00:46
728x90
반응형

V2. 엔티티를 DTO로 변환

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() {
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());

        List<SimpleOrderDto> result = orders.stream()
                .map(o -> new SimpleOrderDto(o)) // Order를 SimpleOrderDto로 변환
                .collect(Collectors.toList());

        return result;
    }
    
    // DTO
    @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(); //LAZY 초기화
            orderDate = order.getOrderDate();
            orderStatus = order.getOrderStatus();
            address = order.getDelivery().getAddress(); //LAZY 초기화
        }
    }
}

 

DTO를 통해 반환했을 때의 장점

  • 엔티티가 바뀌어도 API의 스펙이 깨지지 않는다.

 

문제점

  • 지연 로딩으로 인한 데이터베이스 쿼리가 많이 호출된다.
  • 쿼리가 N + 1번 실행된다. (성능 저하)
    • order 조회 1번 (order의 조회 결과 수가 N이 된다.)
    • order -> member  지연 로딩 조회 N번
    • order -> delivery   지연 로딩 조회 N번
    • 예) order의 결과가 2개면 1 + 2 + 2번 실행된다. (최악의 경우)
      • 지연 로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.
      • 예) 같은 회원이 주문한 경우, 이미 조회된 회원이기 때문에 쿼리가 줄어든다.

 

해결 방법

  • 페치 조인을 사용하면 N + 1 문제를 해결할 수 있다.

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

  • N + 1 문제를 해결하기 위해 페치 조인을 사용하기
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    @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;
    }
}

 

OrderRepository

public List<Order> findAllWithMemberDelivery() {
    em.createQuery(
        "select o from Order o" +
        " join fetch o.member m" +
        " join fetch o.delivery d", Order.class
    ).getResultList();
}
  • V2와 코드는 같지만 리포지토리에서 페치 조인을 사용해준다.

실행 쿼리

select
    order0_.order_id as order_id1_6_0_,
    member1_.member_id as member_i1_4_1_,
    delivery2_.delivery_id as delivery1_2_2_,
    order0_.delivery_id as delivery4_6_0_,
    order0_.member_id as member_i5_6_0_,
    order0_.order_date as order_ad2_6_0_,
    order0_.status as status3_6_0_,
    member1_.city as city2_4_1_,
    member1_.street as street3_4_1_,
    member1_.zipcode as zipcode4_4_1_,
    member1_.name as name5_4_1_,
    delivery2_.city as city2_2_2_,
    delivery2_.street as street3_2_2_,
    delivery2_.zipcode as zipcode4_2_2_,
    delivery2_.status as status5_2_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

그러나 쿼리는 단 한 번만 실행된다.

 

장점

  • 엔티티를 페치 조인을 사용해서 쿼리 1번에 조회할 수 있다.
  • 페치 조인으로 order -> member, order -> delivery는 이미 조회된 상태이므로 지연로딩이 발생하지 않는다.

단점

  • 엔티티의 모든 속성(불필요한 것 포함)을 가져온다.

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

728x90
반응형