ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스프링부트/웹 애플리케이션 개발]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
Designed by Tistory.