내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.
1. 주문 조회 V1: 엔티티 직접 노출
컬렉션 조회같은 경우 1:N 에서 N의 데이터 수가 3건 이라면, JOIN시 1쪽 또한 3건으로 증가해 버린다.
Order - OrderItem - Item 의 연관관계가 (1 : N) (N : 1) 로 되어있는 상황이다.
Order 와 Item의 다대다 관계를 중간 Order_Item 테이블을 통하여 만든 상황이다.
우선 다음 컨트롤러를 확인해 보자.
@RestController
@RequiredArgsConstructor
public class OrderApiController {
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();
order.getDelivery().getAddress();
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName());
}
return all;
}
}
이전 코드와 같이 Order에 지연로딩 처리 되어있던 Member 와 Delivery를 초기화 하고 있다.
Order의 코드는 다음과 같다.
Member와 Delivery는 LAZY 로딩 처리 되어있는것을 확인할 수 있다.
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
}
여기에 추가적으로 상품명도 함께 출력하고 싶은 상황이다.
하지만 Order 안에는 상품명에 대한 정보는 없다. 대신 OrderItem에 대한 정보가 있다.
따라서 order.getOrderItems()를 통해 orderItems를 찾아온 후 접근해야 한다.
해당 OrderItem에 가보면 상품에 대한 이름 정보는 없지만, private Item item 으로 item객체에 대한 참조 정보가 있다.
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item; // 객체에 대한 참조
// 생략...
}
item에 대한 참조가 LAZY 로딩 되어있기 때문에 item에 대한 데이터 또한 가져오기 위해 위에서 다음과 같이 한 것 이다.
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName());
실행 결과는 다음과 같다. 하나의 Item에 대한 정보가 다음과 같다.
member, orderItem, delivery 에 대한 정보를 출력하고 있다.
[
{
"id": 4,
"member": {
"id": 1,
"name": "userA",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
}
},
"orderItems": [
{
"id": 6,
"item": {
"id": 2,
"name": "JPA1 BOOK",
"price": 10000,
"stockQuantity": 99,
"categories": null,
"author": null,
"isbn": null
},
"orderPrice": 10000,
"count": 1,
"totalPrice": 10000
},
{
"id": 7,
"item": {
"id": 3,
"name": "JPA2 BOOK",
"price": 20000,
"stockQuantity": 98,
"categories": null,
"author": null,
"isbn": null
},
"orderPrice": 20000,
"count": 2,
"totalPrice": 40000
}
],
"delivery": {
"id": 5,
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"status": null
},
"orderDate": "2022-04-22T08:22:31.598277",
"status": "ORDER",
"totalPrice": 50000
},
// 생략...
]
하지만 위와 같이 Entity를 직접적으로 반환하는 것 은 좋지 못하다.
이에대한 내용은 이어서 알아보자.
2. 주문 조회 V2: 엔티티를 DTO로 변환
이번에는 Entity를 직접 반환하는 것 이 아닌, DTO 로 변환하여 반환해 보자.
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> collect = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
@Getter
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 order) {
orderId = order.getId();
name = order.getMember().getName(); // LAZY 로딩 초기화
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // LAZY 로딩 초기화
orderItems = order.getOrderItems();
}
}
실행결과는 다음과 같다.
orderItems에 대한 부분은 null로 반환되었다. 이는 Hibernate5Module을 Bean을 등록해줬기 때문에 Proxy는 무시하고 출력한 것 이다.
즉, OrderItems이 lazy로딩이고 프록시객체를 통해 OrderItems에 더이상 탐색을 안했기때문에 Hibernate5Module 로 인해 null로 나온 것 이다.
여기서 orderItems 까지 출력하려면 코드가 다음과 같이 지저분 하기는 하지만 다음과 같이 forEach문을 돌면서 초기화를 해주면 된다.
@Getter
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 order) {
orderId = order.getId();
name = order.getMember().getName(); // LAZY 로딩 초기화
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // LAZY 로딩 초기화
order.getOrderItems().stream().forEach(o -> o.getItem().getName()); // 초기화 부분
orderItems = order.getOrderItems();
}
}
실행 결과는 다음과 같다.
아직 문제점이 남아있다.
DTO를 반환할때는 사실 DTO안에 Entity가 있으면 안된다.
위 코드같은 경우 OrderDto를 통해 List<OrderItem>과 같은 List를 Wrapping 하여 Entity를 반환하고 있는데, 이러면 안된다!
Wrapping하기는 했지만, 사실상 Entity를 직접 반환하는 것 이다.
위 결과를 보면 OrderItem에 대한 Entity가 외부에 다 노출되어 버렸다. 따라서 OrdetItem 까지 DTO로 변환 시켜야 한다.
변환시킨 코드는 다음과 같다.
@Getter
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 order) {
orderId = order.getId();
name = order.getMember().getName(); // LAZY 로딩 초기화
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // LAZY 로딩 초기화
orderItems = order.getOrderItems()
.stream()
.map(OrderItemDto::new)
.collect(Collectors.toList());
}
}
@Getter
static class OrderItemDto {
private String itemName;
private int orderPrice;
private int count;
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
실행 결과는 다음과 같다.
만들어준 DTO 대로 잘 나오는 것 을 확인할수 있다.
그럼 이번에는 SQL문이 총 몇번 나가게 될까?
1) 맨 처음 Order를 검색하는 쿼리 1번 (2개의 Order 검색됨)
2) 1번 Order의 Member, delivery, OrderItems 를 가져온다. 쿼리 3번
2-1) OrderItems에서 Item을 2개 가져오게 된다. 쿼리 2번
3) 2번 Order의 Member, delivery, OrderItems 를 가져온다. 쿼리 3번
3-1) OrderItems에서 Item을 2개 가져오게 된다. 쿼리 2번
총 11번의 쿼리가 발생!
이러한 컬렉션에서의 N+1의 문제를 다음 시간에 Fetch Join을 통해 극복해 보자!
'BackEnd > JPA' 카테고리의 다른 글
[JPA] 컬렉션 조회 최적화 - 3 (0) | 2022.04.24 |
---|---|
[JPA] 컬렉션 조회 최적화 - 2 (0) | 2022.04.23 |
[JPA] 지연 로딩과 조회 성능 최적화 - 2 (0) | 2022.04.20 |
[JPA] 지연 로딩과 조회 성능 최적화 - 1 (0) | 2022.04.19 |
[JPA] 변경감지와 병합(merge) (0) | 2022.04.11 |
댓글