본 글은 인프런의 김영한님 강의 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
을 수강하며 기록한 필기 내용을 정리한 글입니다.
-> 인프런
-> 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의
@Data
public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus status;
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.status = orderStatus;
this.address = address;
}
}
@Data
@AllArgsConstructor
public class OrderItemQueryDto {
private String itemName;
private int orderPrice;
private int count;
}
public interface OrderQueryRepository {
List<OrderQueryDto> findOrderQueryDtos();
List<OrderQueryDto> findOrders();
}
@Repository
@RequiredArgsConstructor
public class OrderQueryRepositoryImpl implements OrderQueryRepository {
private final EntityManager em;
@Override
public List<OrderQueryDto> findOrderQueryDtos() {
List<OrderQueryDto> result = findOrders();
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
@Override
public List<OrderQueryDto> findOrders() {
return em.createQuery(
"SELECT new jpabook.jpashop.domain.order.dao.dtorepository.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();
}
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery(
"SELECT new jpabook.jpashop.domain.order.dao.dtorepository.OrderItemQueryDto(i.name, oi.orderPrice, oi.count) "
+ "FROM OrderItem oi "
+ "JOIN oi.item i "
+ "WHERE oi.order.id = :orderId", OrderItemQueryDto.class
)
.setParameter("orderId", orderId)
.getResultList();
}
}
findOrderQueryDtos()
를 통해 조회 로직이 수행되며, 다음과 같이 동작한다.findOrders()
실행 : List<OrderQueryDto> result = findOrders();
@~~ToOne
연관관계를 갖는 엔티티들만 JOIN 해서 JPA에서 DTO로 직접 조회하도록 설정한다.OrderQueryDto
생성자를 통해 데이터들이 들어간다.OrderQueryDto
의 orderItems 필드는 비어있다.findOrderItems()
실행 : List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
@~~ToOne
관계이므로 JOIN을 해도 결과 row 수가 뻥튀기 되지 않는다.→ N + 1 번 쿼리가 나간다.
@Override
public List<OrderQueryDto> findOrderQueryDtosOptimized() {
List<OrderQueryDto> result = findOrders();
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
private List<Long> toOrderIds(List<OrderQueryDto> result) {
List<Long> orderIds = result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
return orderIds;
}
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
Map<Long, List<OrderItemQueryDto>> orderItemMap = em.createQuery(
"SELECT new jpabook.jpashop.domain.order.dao.dtorepository.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()
.stream()
.collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
return orderItemMap;
}
findOrders()
메서드로 OrderQueryDto 를 조회한다. : List<OrderQueryDto> result = findOrders();
toOrderIds()
메서드로 OrderQueryDto 조회 결과로부터 orderId 값들을 추출하여 List로 묶는다.findOrderItemMap()
메서드로 OrderItemQueryDto를 조회하고, orderId와 매핑시켜 Map 을 생성한다.Map<Long, List<OrderItemQueryDto>> orderItemMap
을 다음 과정으로 완성시킨다.OrderItemQueryDto::getOrderId
) 를 key 값으로 두고, key 값에 해당하는 value 값들을 Map으로 묶어주는 역할을 수행한다.orderId : List<OrderItemQueryDto>
로 key : value 쌍이 만들어진다.result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
@Data
@AllArgsConstructor
public class OrderFlatDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus status;
private Address address;
private String itemName;
private int orderPrice;
private int count;
}
@Override
public List<OrderFlatDto> findOrderQueryDtosFlat() {
return em.createQuery(
"SELECT new jpabook.jpashop.domain.order.dao.dtorepository.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();
}
@GetMapping("/order/v6")
public OrderResponseFormat<List<OrderQueryDto>> ordersV6() {
List<OrderFlatDto> flats = orderService.findOrderQueryDtosFlat();
return new OrderResponseFormat<>("조회 완료",
flats
.stream()
.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getOrderDate(), o.getStatus(), 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().getStatus(), e.getKey().getAddress(), e.getValue()))
.collect(toList())
);
}
⇒ 엔티티로 조회하던 V3.1까지의 과정과 비교해 보면, JPA에서 DTO로 바로 조회하는 V4~V6과정이 좀 더 복잡한 것을 확인할 수 있다. 하지만 V4~V6 과정을 적용하면 DB로 전송되는 쿼리에서 SELECT 절을 줄일 수 있다.
(불필요한 컬럼들까지 조회할 필요가 없어진다. 하지만 그만큼 API 스펙에 딱 맞춰진 조회 메서드로 구성될 것이다.)