-
[스프링부트/웹 애플리케이션 개발]API 개발 고급 - 컬렉션 조회 최적화 -4,5,6스프링&스프링부트 2023. 1. 16. 22:25
주문조회 V4: JPA에서 DTO 직접 조회
OrderApiController
@RestController @RequiredArgsConstructor public class OrderApiController { private final OrderRepository orderRepository; private final OrderQueryRepository orderQueryRepository; @GetMapping("/api/v4/orders") public List<OrderQueryDto> ordersV4() { return orderQueryRepository.findOrderQueryDto(); } }
OrderQueryRepository
package jpabook.jpashop.repository.order.query; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import javax.persistence.EntityManager; import java.util.List; @Repository @RequiredArgsConstructor public class OrderQueryRepository { private final EntityManager em; /** * 컬렉션은 별도로 조회 * Query: 루트 1번, 컬렉션 N 번 * 단건 조회에서 많이 사용하는 방식 */ public List<OrderQueryDto> findOrderQueryDto() { //루트 조회(toOne 코드를 모두 한번에 조회) List<OrderQueryDto> result = findOrders(); //루프를 돌면서 컬렉션 추가(추가 쿼리 실행) result.forEach(o -> { List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId()); o.setOrderItems(orderItems); }); return result; } /** * 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(); } /** * 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(); } }
OrderItemQueryDto
package jpabook.jpashop.repository.order.query; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; @Data public class OrderItemQueryDto { @JsonIgnore private Long orderId; private String itemName; private int orderPrice; private int count; public OrderItemQueryDto(Long orderId, String itemName, int orderPrice, int count) { this.orderId = orderId; this.itemName = itemName; this.orderPrice = orderPrice; this.count = count; } }
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.LocalDate; import java.time.LocalDateTime; import java.util.List; @Data @EqualsAndHashCode(of = "orderId") 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; } }
- Query: 루트 1번, 컬렉션 N 번 실행 > 단점
- ToOne(N:1, 1:1) 관계들을 먼저 조회하고, ToMany(1:N) 관계는 각각 별도로 처리한다.
> 이런 방식을 선택한 이유는 다음과 같다.
> ToOne 관계는 조인해도 데이터 row 수가 증가하지 않는다.
> ToMany(1:N) 관계는 조인하면 row 수가 증가한다.
- row 수가 증가하지 않는 ToOne 관계는 조인으로 최적화하기 쉬우므로 한 번에 조회하고, ToMany 관계는 최적화하기 어려우므로 findOrderItems() 같은 별도의 메서드로 조회한다.
주문조회 V5: JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화
OrderApiController
@GetMapping("/api/v5/orders") public List<OrderQueryDto> ordersV5() { return orderQueryRepository.findAllByDto_optimization(); }
OrderQueryRepository
public List<OrderQueryDto> findAllByDto_optimization() { List<OrderQueryDto> result = findOrders(); List<Long> orderIds = result.stream() .map(o -> o.getOrderId()) .collect(Collectors.toList()); List<OrderItemQueryDto> orderItems = 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 in :orderIds", OrderItemQueryDto.class) .setParameter("orderIds", orderIds) .getResultList(); Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream() .collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId())); result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId()))); return result; }
public List<OrderQueryDto> findAllByDto_optimization() { List<OrderQueryDto> result = findOrders(); //List<Long> orderIds = toOrderIds(result); //Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(orderIds); Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result)); result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId()))); return result; } private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) { List<OrderItemQueryDto> orderItems = 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 in :orderIds", OrderItemQueryDto.class) .setParameter("orderIds", orderIds) .getResultList(); Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream() .collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId())); return orderItemMap; } private static List<Long> toOrderIds(List<OrderQueryDto> result) { List<Long> orderIds = result.stream() .map(o -> o.getOrderId()) .collect(Collectors.toList()); return orderIds; }
- Query: 루트 1번, 컬렉션 1번
- ToOne 관계들을 먼저 조회하고, 여기서 얻은 식별자 orderId로 ToMany 관계인 OrderItem 을 한꺼번에 조회
- MAP을 사용해서 매칭 성능 향상(O(1))
주문조회 V6: JPA에서 DTO 직접 조회 - 플랫 데이터 최적화
OrderApiController
@GetMapping("/api/v6/orders") public List<OrderQueryDto> ordersV6() { return orderQueryRepository.findAllByDto_flat(); }
@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()); }
OrderQueryDto에 생성자 추가
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 OrderStatus orderStatus; private Address address; 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; } }
OrderQueryRepository
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 OrderStatus orderStatus; private Address address; 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'스프링&스프링부트' 카테고리의 다른 글
[스프링부트/웹 애플리케이션 개발]API 개발 고급 - 실무 필수 최적화 (0) 2023.01.17 [스프링부트/웹 애플리케이션 개발]API 개발 고급 정리 (0) 2023.01.16 [스프링부트/웹 애플리케이션 개발]API 개발 고급 - 컬렉션 조회 최적화 -3 (0) 2023.01.13 [스프링부트/웹 애플리케이션 개발]API 개발 고급 - 컬렉션 조회 최적화 -1,2 (0) 2023.01.13 [스프링부트/웹 애플리케이션 개발]API 개발 고급-5 (2) 2023.01.12