대충 넘어가지 않는 습관을 위한 기록

JPA 기록 - 지연 로딩과 조회 성능 최적화

uhyvn 2024. 4. 23. 17:21

김영한님의 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 를 보고 기록한 글입니다.

 

 

 


 

 

 

 

기본적으로 페치 타입 LAZY를 사용한다는 가정 하에, 아래부터 한 단계씩 최적화하는 방법을 기록하려고 한다.

 

참고 테이블

( 참고: 회원이 주문을 하기 때문에, 회원이 주문리스트를 가지는 것은 얼핏 보면 잘 설계한 것 같지만, 객체 세상은 실제 세계와는 다르다.

무에서는 회원이 주문을 참조하지 않고, 주문이 회원을 참조하는 것으로 충분하다.

여기 서는 일대다, 다대일의 양방향 연관관계를 설명하기 위해서 추가했다. )

 

 


 

 

 

 

우선 첫번째 코드는, 기본적으로 제공하는 findAll()을 사용하는 코드다.

/**
 * V1. 엔티티를 조회해서 DTO로 변환(fetch join 사용X)
 * - 단점: 지연로딩으로 쿼리 N번 호출
 */
@GetMapping("/api/v1/simple-orders")
public List<SimpleOrderDto> ordersV1() {
   List<Order> orders = orderRepository.findAllByString(new OrderSearch());
   List<SimpleOrderDto> result = orders.stream()
         .map(o -> new SimpleOrderDto(o))
         .collect(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();
     orderDate = order.getOrderDate();
     orderStatus = order.getStatus();
     address = order.getDelivery().getAddress();
   }
}

 

 

위 코드를 실행한다면, 쿼리가 총 1 + N + N번 실행된다.

  • order 조회 1번(order 조회 결과 수가 N이 된다.)
  • order -> member 지연 로딩 조회 N 번
  • order -> delivery 지연 로딩 조회 N 번
  • 예) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다.(최악의 경우)
    • 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.

 

 

 

 

 


 

 

 

 

 

페치 조인을 써서 최적화한 두 번째 코드

/**
 * V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용O)
 * - fetch join으로 쿼리 1번 호출
 * 참고: fetch join에 대한 자세한 내용은 JPA 기본편 참고(정말 중요함)
 */
@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(toList());
   return result;
}

 

OrderRepository - 추가 코드

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로 바로 조회하는 코드다.

private final OrderSimpleQueryRepository orderSimpleQueryRepository; //의존관계 주입
/**
 * V4. JPA에서 DTO로 바로 조회
 * - 쿼리 1번 호출
 * - select 절에서 원하는 데이터만 선택해서 조회
 */
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
   return orderSimpleQueryRepository.findOrderDtos();
}

 

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

 

OrderSimpleQueryDto 리포지토리에서 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;
   }
}

 

  • 일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회
  • new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
  • SELECT 절에서 원하는 데이터를 직접 선택하므로 DB 애플리케이션 네트웍 용량 최적화(생각보다 미비)
  • 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

 

 


 

 

 

 

 

 

 

마지막 두 개의 최적화 (엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법) 는 각각 장단점이 있다.

둘중 상황에 따라서 더 나은 방법을 선택하면 된다.

그러나 dto조회와 엔티티 조회 시 쿼리 필드 수가 많이 차이나지 않는다면,

엔티티로 조회하면 리포지토리 재사용성도 좋고, 개발도 단순해진다.

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

 

 

쿼리 방식 선택 권장 순서

  1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
  2. 필요하면 페치 조인으로 성능을 최적화 한다. -> 대부분의 성능 이슈가 해결된다.
  3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
  4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.