CS 잡지식
DTO는 그 어떠한 [도메인 엔티티]를 의존해서는 안된다
JIN_YOUNG _KIM
2023. 5. 4. 16:25
package jpabook.jpashop.controller;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
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;
import static java.util.stream.Collectors.toList;
/**
* V1. 엔티티 직접 노출
* - Hibernate5Module 모듈 등록, LAZY=null 처리
* - 양방향 관계 문제 발생 -> @JsonIgnore
*/
/**
* 여태껏 엄청난 착각을 하였다.
* order.getMember() : Lazy Member 초기화 x
* order.getMember().getName() : Lazy Member 초기화 o
* -> Lazy 객체의 메서드를 호출해야지, 초기화가 되는 거였다.
*/
@RestController
@RequiredArgsConstructor
public class OrderApiController { //[주문 내역]에서 주문한 [상품 정보(OrderItem,Item필요)]를
// 추가로 조회하는 API
private final OrderRepository orderRepository;
@GetMapping("/api/v1/orders")
public List<Order> ordersV1(){ // [도메인 엔티티]를 반환하고 있는 안 좋은 예시.
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); // Lazy 객체 강제 초기화
order.getDelivery().getAddress(); // Lazy 객체 강제 초기화
List<OrderItem> orderItems = order.getOrderItems();
// for (OrderItem orderItem : orderItems) {
// orderItem.getItem().getName(); // Lazy 객체 강제 초기화
// }
orderItems.stream().forEach(o->o.getItem().getName()); // 위 for문을 Lamda식으로 변경
}
return all;
}
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2(){ //[도메인 엔티티]를 DTO로 [한 번] 감싸서 전달.
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
// Lazy 객체 강제 초기화는 OrderDto 클래스에서 구현해 놓음.
// 이 반복문을 통해 그 구현이 실행됨으로서 초기화가 일어남.
// 이 시점에는 아직 orderRepository에서 OrderItem, Item에 대한 fetch Join JPQL이 없으므로,
// 아래 람다에서 반복문이 일어 날때마다 조회하는 SQL문이 날라간다.(N+1문제 V3에서 Fetch Join으로 최적화 할 예정)
// 그러나, 컬렉션을 fetch join할 때 주의해야 할 부분이 있다고 말씀하심(추후에 설명 예정이라고 함)
List<OrderDto> all = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return all;
}
@Data
static class OrderDto { // API의 결과 스펙
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private OrderStatus orderStatus;
private Address address;
// private List<OrderItem> orderItems; // OrderItem는 [도메인 엔티티]이다.
// 우리는 [도메인 엔티티]를 직접 전달하지 않기 위헤서, DTO로 [한 번] 감싸서 보내면 괜찮겠지 싶어서 보냈지만,
// DTO 안에서 조차, [도메인 엔티티]가 있으면 안된다(이를 "DTO가 [도메인 엔티티]에 의존하고 있다"라고 표현)
// 전달하는 DTO에서 조차, [도메인 엔티티]를 의존하고 있으면 안된다.
// 왜냐하면, 클라이언트에게 이 DTO가 뿌려지게 되면, OrderItem [도메인 엔티티]가 그대로 노출되기 때문.
// 결론 : 전달되는 DTO는 그 어떠한 [도메인 엔티티]도 의존하고 있으면 안된다.
// [도메인 엔티티]에 대한 의존이 없어지도록, 해당 [도메인 엔티티]를 별도의 DTO를 생성해서, 다시 한번
// 감싸서 보내야 함.(OrderItem을 OrderItemDto 클래스로 Wrapping 하자)
private List<OrderItemDto> orderItems;// OrderItemDto : 아래에 구현함
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName(); // Lazy 객체 강제 초기화
orderDate = order.getOrderDate();
orderStatus = order.getOrderStatus();
address = order.getDelivery().getAddress(); // Lazy 객체 강제 초기화
orderItems = order.getOrderItems().stream() // Lazy 객체 강제 초기화
.map(orderItem -> new OrderItemDto(orderItem))
.collect(toList());
}
}
@Data
static class OrderItemDto {
private String itemName;//상품 명
private int orderPrice; //주문 가격
private int count; //주문 수량
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName(); // Lazy 객체 강제 초기화
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
}