0. 개요
- 이전 포스팅까지 Many To One 또는 One To One 관계에 대한 조회 방식을 알아보았다.
- To One 관계는 Join 대상이 하나인 관계로, Fetch Join을 이용하여 N + 1 문제를 해결하였다.
- To Many의 관계는 Join 대상이 다수인 관계로, 중복 데이터가 발생하는 문제를 가진다.
- 이번 포스팅에서 To Many 관계를 조회할 때 발생하는 문제와 해결책에 대해서 알아보자.
1. 컬렉션 조회 시 DTO 변환 방식
a) 예시 코드
- 다음과 같이 DTO를 사용하여 컬렉션 조회를 수행하려고 한다.
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
// DTO 변환 방식
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o)).collect(Collectors.toList());
return result;
}
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItem> orderItems;
public OrderDto(Order o) {
orderId = o.getId();
name = o.getMember().getName();
orderDate = o.getOrderDate();
orderStatus = o.getStatus();
address = o.getDelivery().getAdderss();
// OrderItem 또한 Entity 이므로 Lazy Loading을 수행해야된다.
o.getOrderItems().stream().forEach(order -> order.getItem().getName()); //
orderItems = o.getOrderItems();
}
}
}
- 위의 코드를 실행하여 Postman으로 확인해보면 문제없이 결과가 잘 나오는 것을 확인할 수 있다.
- 다만, 여기서 한 가지 간과하는 것이 있다.
b) DTO 내부에 Entity가 존재한다.
- DTO의 코드를 살펴보면 내부에 OrderItem이 그대로 사용된 것을 확인할 수 있다.
- 이처럼 DTO 내부에 Entity를 그대로 사용하는 것은 Entity를 그대로 노출하는 것과 동일한 방식이다.
- 그러므로 DTO 내부에서 사용되는 Entity 또한 DTO로 만들어줘야 한다.
- 왜냐면 OrderItem의 내용이 변경되는 순간, API 스펙도 변경되어야 하기 때문이다.
- 이 부분이 골치가 아파지는 부분이다. 그렇다면 어떻게 해결해야 할까?
c) DTO를 만든다.
- 해결방법은 간단하다. 해당 Entity에 대한 DTO를 생성하면 된다.
- 이는 다음과 같이 작성할 수 있다.
@Data
static class OrderItemDto {
// OrderItem 내부의 속성 중 필요한 것만 선언한다.
private String itemName;
private int orderPrice;
private int count;
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
- 이제 기존의 DTO에서 Entity가 직접 사용된 부분을 생성한 DTO로 변경하면 된다.
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
public OrderDto(Order o) {
orderId = o.getId();
name = o.getMember().getName();
orderDate = o.getOrderDate();
orderStatus = o.getStatus();
address = o.getDelivery().getAdderss();
orderItems = o.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(Collectors.toList());
}
}
d) DTO 변환 방식의 문제점
- 위의 코드를 실행하면 몇 개의 SQL이 발행될까?
- 우선 Order 테이블이 조회되고 내부의 Member, Delivery, OrderItem, Item이 조회된다.
- 여기서 끝이 아니다. Member, Delivery, OrderItem, Item 테이블이 주문을 요청한 회원의 수만큼 조회된다.
- 예를 들어, 2명의 회원이 각각 유니크한 2가지 종류의 아이템을 주문한다.(총 4가지 종류의 아이템이 존재함)
- 그렇게 총 2개의 주문이 존재한다고 해보자.
- 우선 최초에 Order를 1번 조회하는 SQL이 발행된다.
- 그리고 첫 번째 회원의 정보를 얻기 위해 Member를 조회하는 SQL이 발행된다.
- 회원 정보를 기반으로 Delivery와 OrderItem을 조회하는 SQL이 각 1번씩 발행된다.
- OrderItem 내부의 Item을 찾기 위해서 Item을 조회하는 SQL이 2번 발행된다.(하나의 주문당 아이템이 2개 존재함)
- 결과적으로 Member부터 Item을 조회화는 과정이 한번 더 반복된다. (회원이 총 2명이기 때문)
- 이 과정을 정리해보자면 다음과 같다.
- Order 조회 1번(= 2개의 주문을 찾음), Member 조회 2번(= 회원이 2명)
- Delievery 조회 2번(= 주문이 2건), OrderItem 조회 2번(= 주문이 2건)
- Item 조회 4번(= 주문당 유니크한 아이템이 2가지, 총 2개의 주문이므로 4가지 종류의 아이템이 존재)
- 총 11개의 SQL이 발행되는 것이다.
e) 해결 방법
- 그렇다면 이 문제를 어떻게 해결할 수 있을까?
- 아마 Fetch Join을 사용해서 해결할 수 있지 않을까 생각이 들 것이다.
- 다음 포스팅에서 컬렉션 조회 시 Fetch Join을 사용하는 방법에 대해서 알아보자.
'Back-end > JPA 개념' 카테고리의 다른 글
API 조회 성능 최적화 - 컬렉션(Fetch Join + 페이징 + Batch Size) (0) | 2022.06.03 |
---|---|
API 조회 성능 최적화 - 컬렉션(Fetch Join, distinct) (0) | 2022.06.02 |
API 조회 성능 최적화 - DTO 직접 조회 (0) | 2022.05.30 |
API 조회 성능 최적화 - DTO 변환 방식(Fetch Join) (0) | 2022.05.30 |
API 조회 성능 최적화 - Entity 직접 노출 방식 (0) | 2022.05.28 |
댓글