주문 + 배송정보 + 회원을 조회하는 API
지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결!
간단한 주문 조회 V1: 엔티티를 직접 노출 → 사용하면 안 됨!
- 양방향이 걸리는 곳에 @JsonIgnore을 해주어야 함
- 지연로딩 LAZY로 되어 있다면 실제 엔티티 대신에 프록시를 가져옴
- 순수한 자바 객체가 아니므로 JSON에서 오류가 발생함
- Hibernate5Module 을 스프링 빈으로 등록하면 해결(스프링 부트 사용중)
간단한 주문 조회 V2: 엔티티를 DTO로 변환
엔티티를 DTO로 변환하는 일반적인 방법
- 쿼리가 총 1 + N + N번 실행됨
- order 조회 1번(order 조회 결과 수 N)
- order -> member 지연 로딩 조회 N 번, order -> delivery 지연 로딩 조회 N 번
- ex) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다.(최악의 경우) 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다
OrderSimpleApiController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() { // 원래는 List로 한 번 감싸야 함!
//N + 1 문제 -> 첫번째 쿼리의 결과로 N번만큼 쿼리가 추가 실행되는 것
//1 + 회원(N) + 배송(N)
List<Order> orders = orderRepository.findAllByCriteria(new OrderSearch());
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return result;
}
@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.getStatus();
address = order.getDelivery().getAddress(); // LAZY 초기화
}
}
}
간단한 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화
- 여러 API에서 사용 가능 -> 원하는 DTO로
OrderSimpleApiController
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() { // 원래는 List로 한 번 감싸야 함!
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() {
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
간단한 주문 조회 V4: JPA에서 DTO로 바로 조회
OrderQuerySimpleDto -> Repository에서 Controller 역참조 하면 안 되므로 생성
@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; // LAZY 초기화
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address; // LAZY 초기화
}
}
OrderSimpleApiController
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() { // 원래는 List로 한 번 감싸야 함!
return orderRepository.findOrderDtos();
}
OrderRepository
public List<OrderSimpleQueryDto> findOrderDtos() {
return em.createQuery("select new jpabook.jpashop.repository.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을 사용할 때 처럼 원하는 값을 선택해서 조회
- new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
- SELECT 절에서 원하는 데이터를 직접 선택하므로 DB 애플리케이션 네트웍 용량 최적화(생각보다 미비)
- 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점
쿼리 방식 선택 권장 순서
- 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
- 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다. (v3)
- 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다. (v4)
- 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.
참고 강의 :
실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 - 인프런 | 강의
스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
'JPA > 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화' 카테고리의 다른 글
[스프링 부트와 JPA 활용2 - API 개발과 성능 최적화] 3. API 개발 고급 - 컬렉션 조회 최적화(2) (0) | 2023.04.29 |
---|---|
[스프링 부트와 JPA 활용2 - API 개발과 성능 최적화] 3. API 개발 고급 - 컬렉션 조회 최적화(1) (0) | 2023.03.22 |
[스프링 부트와 JPA 활용2 - API 개발과 성능 최적화] 2. API 개발 고급 - 준비 (0) | 2023.01.10 |
[스프링 부트와 JPA 활용2 - API 개발과 성능 최적화] 1. API 개발 기본 - 회원 API (0) | 2023.01.09 |