(JPA) 지연 로딩 및 쿼리 성능 최적화

(김영한의 실습! Spring Boot와 JPA Utilization 2 강의를 듣고 정리한 내용입니다.)

소개

  • xToOne(ManyToOne, OneToOne) 관계에서 성능을 최적화하자.
  • 여기에서는 주문을 검색하는 API를 예로 들었습니다.
  • 참고로 Order와 Member는 ManyToOne 관계이고 Delivery는 OneToOne 관계입니다.

색인

  1. 주문 조회 V2(엔티티를 DTO로 변환)
  2. 주문 조회 V3(엔티티를 DTO로 변환 – 조인 최적화 가져오기)
  3. 주문 조회 V4(JPA에서 DTO로 직접 조회)

1. 주문 조회 V2(Entity를 DTO로 변환)

  • 이는 엔터티를 DTO로 변환하는 일반적인 방법입니다.
@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))
            .collect(Collectors.toList());
    return result;
}

쿼리는 총 1 + N + N 번 실행됩니다. (주문 추출 쿼리 1회 + order.member 지연 로딩 조회 N회 + 주문.배송 Lazy loading query N times) 주문이 3개라면 총 1 + 3 + 3 = 7개의 query를 수행한다.

2. 주문 조회 V3(엔티티를 DTO로 변환 – 조인 최적화 가져오기)

V2와 동일하지만 주문 추출을 위한 쿼리문이 다릅니다. findAllwithMemberDelivery를 재정의합시다.

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

아래와 같이 Fetch Join을 이용하여 오더를 가져오면 연결된 멤버 테이블과 배송 테이블이 한번에 묶음으로 가져옵니다.

(주문 저장소)

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

결과적으로 클라이언트는 아래와 같이 질의문 1로 응답할 수 있다.


3. 주문 조회 V4(JPA에서 DTO로 직접 조회)

아래와 같이 클라이언트에 응답하기 위해 Repository에서 Dto를 반환하는 쿼리가 생성됩니다.

응답을 위해 OrderSimpleQueryDto 및 findOrderDtos() 메서드를 생성해야 합니다.

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

리포지토리는 순수 엔터티로 작동하는 쿼리를 작성하는 데 적합합니다.

따라서 쿼리를 작성하기 위해 별도의 OrderSimpleQueryRepository가 생성됩니다.

(OrderSimpleQueryRepository)

@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {
    private final EntityManager em;

    public List<OrderSimpleQueryDto> findOrderDtos(){
        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();
    }
}

일반 SQL을 사용하는 경우와 같이 원하는 값을 선택하여 조회할 수 있습니다.

JPQL 결과는 새 명령을 사용하여 OrderSimpleQueryDto로 직접 변환되었습니다.

(OrderSimpleQueryDto)

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

결과는 아래와 같이 V3로 조인한 부분과 동일하지만 원하는 테이블의 속성을 가져오는 부분에서 차이가 있습니다. (SELECT 절에서 원하는 데이터를 직접 선택합니다.)


정리하다

  • 엔터티를 DTO로 변환하거나 DTO로 직접 쿼리하는 것에는 장단점이 있습니다.
  • 둘 중 어느 것이 더 나은지는 상황에 따라 다릅니다.
  • 엔터티별 쿼리는 리포지토리 재사용성을 개선하고 개발을 간소화합니다.

질의 방법을 선택하는 권장 순서는 다음과 같습니다.

  1. 엔터티를 DTO로 변환하는 방법을 선택합니다.(V2)
  2. 필요한 경우 페치 조인으로 성능을 최적화합니다. <- 이것은 대부분의 성능 문제를 해결합니다.(V3)
  3. DTO에 직접 조회하는 방법을 사용합니다.(V4)
  4. JPA에서 제공하는 Native SQL이나 Spring JDBC Template SQL을 사용합니다.