주문 , 배송정보 , 회원을 조회하는 API를 여러방법으로 만들어봅니다.그러면서 지연로딩 때문에 발생하는 성능문제를 단계적으로 해결해보자.
간단한 주문조회 V1 :엔티티를 직접 노출
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* xToOne(ManyToOne, OneToOne
* Order
* Order -> Member
* Order -> Delivery
*/
@RequiredArgsConstructor
@RestController
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1(){
List<Order> all = orderRepository.findAllByString(new OrderSearch());
return all;
}
}
엔티르르 직접 노출하는것은 좋지않다!!
해당 API를 호출하면 Order<->Member와 Order<->delivery간 엔티티를 조회하는것이 무한순회하면서
에러가 발생하게 된다.

@JsonIgnore

해결책
-> Hibernate5Module 을 스프링 Bean으로 등록하면 해결된다.
/*build.gradle*/
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
/*JpashopApplication.java*/
@Bean
Hibernate5Module hibernate5Module(){
return new Hibernate5Module();
}
gradle implentation 할때 version을 명시해주지않으면 gradle에서 자동으로 현재버전과 잘맞는 버젼을import시켜준다.
(Important) 계속 언급했지만 엔티티를 API에 직접 노출하는 것은 좋지 않다. 그래서 Hibernate5Module을 이용하는 방법은 알아만 두고 실무에서는 DTO로 변환해서 반환하는것이 더 좋은 방법이다
간단한 주문 조회 V2: 엔티티를 DTO로 변환
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* xToOne(ManyToOne, OneToOne
* Order
* Order -> Member
* Order -> Delivery
*/
@RequiredArgsConstructor
@RestController
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> collect = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return collect;
}
@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 초기화
}
}
}
V2 버젼으로 API호출했을 때 실행되는 문제.
1+N+N번 실행된것을 확인할수있다.
Order 조회가 많아질수록 기하급수적으로 쿼리수행이 많아지게되고,성능저하게 일어날수있다.
간단한 주문 조회 V3: 엔티티 DTO로 변환 → 페치 조인 최적화
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* xToOne(ManyToOne, OneToOne
* Order
* Order -> Member
* Order -> Delivery
*/
@RequiredArgsConstructor
@RestController
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3(){
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<SimpleOrderDto> collect = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return collect;
}
@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 초기화
}
}
}
...
/*orderRepository.findAllWithMemberDelivery*/
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로 바로 조회
/**
* V4. JPA에서 DTO로 바로 조회
* -쿼리1번 호출
* - select 절에서 원하는 데이터만 선택해서 조회
*/
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderSimpleQueryRepository.findOrderDtos();
}
결론:
엔티티를 DTO로 변환하거나, DTO로 바호 조회하는 두가지방법은 각각 장단점이 있다. 둘 중 상황에 따라서 더 나은
방법을 선택하면된다.
엔티티로 조회하면 리포지토리 재사용성도 좋고,개발도 단순해진다, 따라서 권장하는 방법은 다음과 같다.
쿼리 방식 선택 권장 순서
1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
2. 필요하면 Fetch join으로 성능을 최적화한다. ->대부분의 이슈는 해결될것이다.
3.그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
4.최후의 방법으로 JPA가 제공하는 Native SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접사용한다.