2강과 3강에서는 1강에서 주문을 조회하는 API 개발을 이어나간다.
단, 1강에서 조회한 Order Entity를 직접 Response로 보낸 것과 달리,
적절한 Order 필드만을 추출하여 DTO를 Response로 보내도록 API를 개선하게 된다.
/**
* <주문 조회 API>
* : 모든 주문들을 조회
*
* [버전2] : Entity를 DTO로 변환하여 Response 날림
*
* [문제점]
* : List<Order> 라는 순수 Entity를 DTO로 변환화는 과정에서,
* 각 Order와 연관된 Member, Delivery를 매번 다시 가져와야 하고,
* 그에 따른 쿼리가 각 Order별로 2번씩 더 나가게 된다.
*
* 즉, List<Order>의 Order 개수가 N 이라면, 이 List<Order>를 조회하는 쿼리는 1번만 나가지만,
* N개의 Order에 대해서 연관된 N개의 Member 조회 쿼리가 N번, N개의 Delivery 조회 쿼리가 N번 더 나가게 된다.
*
* [1+N+N] 문제 발생
* */
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2(){
List<Order> all = orderRepository.findAll();
List<SimpleOrderDto> dtoList = all.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return dtoList;
}
@Data
static class SimpleOrderDto{
private Long orderId; //주문 ID
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();
}
}
즉 위와 같이 DTO를 Response로 보내도록 주문 조회 API를 개선하였으나,
여기서 [지연 로딩]에 의한 (1+N) 문제를 마주치게 된다.
<N+1 문제> (== 1+N 문제)
EX) Order와 Member과 ManyToOne으로 연관되있다고 가정
1. 나는 분명 order들을 조회하기 위해, select from orders 쿼리 1방만을 날렸음에도
2. order와 연관된 member를 함께 가져오기 위해, 조회한 N개의 order에 대해 member를 조회하는 쿼리 select from member where member.id=1 이 N번 더 나가게 되는 문제를 말한다.
이는 [지연 로딩]과 [즉시 로딩]에서 모두 나타날 수 있는데,
1_1. [지연 로딩]의 경우에는 , select * from orders로 모든 order를 조회하는 경우에는 쿼리가 1방만 나가게 되지만,
2_1. 이후에 order.getMember().getName() 과 같이, 실제 Member를 사용하는 시점에서 , 조회한 N개의 order와 연관된 Member를 조회하는 쿼리가 N번 더 나가게 된다.1_2. 반면 [즉시 로딩]의 경우에는, select * from orders로 모든 order를 조회할 때 쿼리 1방이 나간 후,
2_2. 아직 Member를 사용하는 코드가 실행되지 않았음에도, 그 즉시 N개의 order와 연관된 member를 조회하는 쿼리가 N번 더 나가게 되는 문제를 말한다.
3_2. 특히 [즉시 로딩]의 경우, [지연 로딩]과 다르게 더 나가게 되는 N개의 쿼리가, 예측할 수 없는 쿼리가 나갈 수 있으므로, 성능 최적화에 어려움을 겪게 된다.
따라서
1. JPA를 사용한 Entity간 연관관계 에서는, 항상 [지연 로딩]으로 관계를 맺어주 되,
2. 1+N 문제가 발생하지 않도록 성능을 최적화 시키기 위해서는, order들을 조회할 때, 연관된 Member나 Delivery를 가져오도록 Fetch Join을 사용하는 방법이 있다!
-> 그러면 실제로 order를 조회하는 1방 쿼리 안에서, inner join으로 각 order와 연관된 member와 delivery를 함께 조회하게 된다!
-> 단 ManyToOne/OneToOne 관계에서의 Fetch Join이므로 데이터 뻥튀기 문제가 일어나지 않음을 주의해야 하고,
-> 이 경우 Fetch Join으로 연관된 Member뿐만 아니라, Member와 Delivery를 모두 가져올 수 있다는 점을 기억해야 한다!
(즉 연관된 Entity 여러개를 Fetch Join으로 함께 가져올 수 있다!)
//4. 모든 order를 다 조회하되, fetch join을 사용하여, order와 연관된 member와 delivery도 함께 조회
//(즉 실제로는 같은 쿼리 내에서 inner join으로 함께 조회함)
public List<Order> findAllWithMemberDelivery(){
List<Order> orders = em.createQuery("select o from Order o " +
"join fetch o.member m join fetch o.delivery d", Order.class)
.getResultList();
return orders;
}
/**
* <주문 조회 API>
* : 모든 주문들을 조회
*
* [버전3]
* : 모든 주문을 조회할 때, 연관된 member와 delivery도 함께 가져오도록 fetch join 사용
* -> 그렇게 하면 실제 order 조회 쿼리가 나갈 때, 연관된 member와 delivery도 함께 가져오도록 inner join 쿼리가 나가기 때문에,
* 실제로는 그 inner join 쿼리 한방으로 모든 order, member, delivery를 함께 가져올 수 있다!
*
* [주목할 점]
* -> 지금까지는 fetch join 사용시 , 연관된 Entity를 하나만 함께 가져오도록 fetch join을 사용했는데
* -> 이로써 연관된 Entity를 여러개 가져오로록 fetch join을 쓸 수 있다는 것을 알게 됨!
* -> 이는 실질적으로 SQL의 inner join 부분을 더 공부해야 할듯!!
* (물론 ManyToOne, OneToOne이여서 데이터 뻥튀기가 안일어 나니깐 fetch join을 맘놓고 사용 가능한 것!!)
* */
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3(){
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<SimpleOrderDto> dtoList = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return dtoList;
}
@Data
static class SimpleOrderDto{
private Long orderId; //주문 ID
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은 [최대 N]이 되는데,
왜냐하면 어떤 order와 연관된 member를 조회했는데,
그 member가 다른 order와도 연관된 경우,
그 order와 연관된 member를 조회할 때에는 , 별도의 쿼리가 나가지 않고
이미 조회하여 영속성 컨텍스트 안에 보관중인 member를 바로 가져다 쓰기 때문이다!
=> 따라서 [조회]의 결과로 반드시 영속성 컨텍스트에 보관함을 기억하고
=> 이를 통해 1+N 문제는 , 사실 [최대] 1+N 이 된다는 디테일도 기억하자!