728x90
728x90
※ 본 포스팅은 김영한 강사님의 인프런 '실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화' 강의를 들으며 작성한 수강일지 입니다.
| API 개발 고급 - 컬렉션 조회 최적화
1. 주문 조회 V1: 엔티티 직접 노출
2. 주문 조회 V2: 엔티티를 DTO로 변환
3. 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화
4. 주문 조회 V3.1: 엔티티를 DTO로 변환 - 페이징과 한계 돌파
5. 주문 조회 V4: JPA에서 DTO 직접 조회
6. 주문 조회 V5: JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화
7. 주문 조회 V6: JPA에서 DTO로 직접 조회, 플랫 데이터 최적화
8. API 개발 고급 정리
주문 조회 V6: JPA에서 DTO로 직접 조회, 플랫 데이터 최적화
OrderApiController에 추가
package jpabook.jpashop.api;
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 jpabook.jpashop.repository.order.query.OrderFlatDto;
import jpabook.jpashop.repository.order.query.OrderItemQueryDto;
import jpabook.jpashop.repository.order.query.OrderQueryDto;
import jpabook.jpashop.repository.order.query.OrderQueryRepository;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.*;
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
private final OrderQueryRepository orderQueryRepository;
// 섹션 4 엔티티 직접 노출
// 원래 이렇게 entity 직접 노출하면 안됨(예제니까 하는 것)
@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;
}
// v2
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch()); // orders를 DTO로 변환해야 하ㅏㅁ
List<OrderDto> collect = orders.stream()
.map(o -> new OrderDto(o)) // 값을 생성자에 넘겨서 dto로 반환
.collect(toList());
return collect;
}
// v3
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem(); // orders를 DTO로 변환해야 함
for(Order order : orders) {
System.out.println("order ref = " + order + " id = " + order.getId());
}
List<OrderDto> collect = orders.stream()
.map(o -> new OrderDto(o)) // 값을 생성자에 넘겨서 dto로 반환
.collect(toList());
return collect;
}
// v3.1
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page(@RequestParam(value = "offeset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "100") int limit) {
List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit); // orders를 DTO로 변환해야 함
List<OrderDto> collect = orders.stream()
.map(o -> new OrderDto(o)) // 값을 생성자에 넘겨서 dto로 반환
.collect(toList());
return collect;
}
// v4 - JPA에서 DTO 직접 조회
@GetMapping("/api/v4/orders")
public List<OrderQueryDto> ordersV4() {
return orderQueryRepository.findOrderQueryDtos();
}
// v5 - JPA에서 DTO 직접 조회
@GetMapping("/api/v5/orders")
public List<OrderQueryDto> ordersV5() {
return orderQueryRepository.findAllByDto_optimization();
}
// @GetMapping("/api/v6/orders")
// public List<OrderFlatDto> ordersV6() {
// return orderQueryRepository.findAllByDto_flat();
// }
// OrderQueryDto에 맞추고 싶다면 내가 중복을 걸려내면 됨
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
return flats.stream()
.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
)).entrySet().stream()
.map(e -> new OrderQueryDto(e.getKey().getOrderId(), e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(), e.getKey().getAddress(), e.getValue()))
.collect(toList());
}
// @Data
@Getter
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private OrderStatus orderStatus;
private Address address; // Entity를 다 노출하면 안됨, but Address 같은 value object는 상관없음
private List<OrderItemDto> orderItems;
// 생성자
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
orderItems = order.getOrderItems().stream() // loop로 돌리면서
// .map(OrderItemDto::new)
.map(orderItem -> new OrderItemDto(orderItem)) // 생성자를 통해 dto로 변환
.collect(toList());
}
}
@Data
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();
}
}
}
OrderQueryDto에 생성자 추가
package jpabook.jpashop.repository.order.query;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.List;
@Data
@EqualsAndHashCode(of = "orderId") // 이걸 기준으로 collect grouping by를 묶어 줌
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
}
public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, List<OrderItemQueryDto> orderItems) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.orderItems = orderItems;
}
}
OrderQueryRepository에 추가
package jpabook.jpashop.repository.order.query;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
/**
* 컬렉션은 별도로 조회
* Query: 루트 1번, 컬렉션 N 번
* 단건 조회에서 많이 사용하는 방식
*/
public List<OrderQueryDto> findOrderQueryDtos() {
//루트 조회(toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders(); //query 1번 -> N개
//루프를 돌면서 컬렉션 추가(추가 쿼리 실행)
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId()); // Query 1번
o.setOrderItems(orderItems);
});
return result;
}
/**
* 1:N 관계(컬렉션)를 제외한 나머지를 한번에 조회
*/
private List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
" from Order o" +
" join o.member m" +
" join o.delivery d", OrderQueryDto.class)
.getResultList();
}
/**
* 1:N 관계인 orderItems 조회
*/
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id = : orderId",
OrderItemQueryDto.class)
.setParameter("orderId", orderId)
.getResultList();
}
// for v5
public List<OrderQueryDto> findAllByDto_optimization() {
List<OrderQueryDto> result = findOrders(); // Query 1번
// // orderId의 list -> 주문번호 4, 11번이 들어있을 것
// List<Long> orderIds = toOrderIds(result);
// // 이전 v4 단점 -> loop를 돔
// Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(orderIds);
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result)); // 주문 데이터 만큼 한방에 메모리에 올리고
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId()))); // loop 돌면서 모자란 부분 채워넣고
return result;
}
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
List<OrderItemQueryDto> orderItems = em.createQuery( // Query 1번 --> 총 2번 나감
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id IN :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
// 이렇게 하면 쿼리 2번으로 최적화 가능
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));// orderIdf를 기준으로 Map으로 변경
return orderItemMap;
}
private static List<Long> toOrderIds(List<OrderQueryDto> result) {
List<Long> orderIds = result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
return orderIds;
}
public List<OrderFlatDto> findAllByDto_flat() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
" from Order o" +
" join o.member m" +
" join o.delivery d" +
" join o.orderItems oi" +
" join oi.item i", OrderFlatDto.class)
.getResultList();
}
}
OrderFlatDto
package jpabook.jpashop.repository.order.query;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class OrderFlatDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private Address address;
private OrderStatus orderStatus;
private String itemName;//상품 명
private int orderPrice; //주문 가격
private int count; //주문 수량
public OrderFlatDto(Long orderId, String name, LocalDateTime orderDate,
OrderStatus orderStatus, Address address, String itemName, int orderPrice, int
count) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
- Query: 1번
- 단점
- 쿼리는 한번이지만 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되 므로 상황에 따라 V5 보다 더 느릴 수 도 있다.
- 애플리케이션에서 추가 작업이 크다.
- 페이징 불가능
728x90
728x90