3 분 소요


주문 조회 V4

JPA에서 DTO 직접 조회

OrderApiController

private final OrderQueryRepository orderQueryRepository;

@GetMapping("/api/v4/orders")
public List<OrderQueryDto> ordersV4() {
    return orderQueryRepository.findOrderQueryDtos();
}

OrderQueryRepository

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    /**
    * 컬렉션은 별도로 조회
    * Query : 루트 1번, 컬렉션 N번
    * 단건 조회에서 많이 사용하는 방식
    */
    public List<OrderQueryDto> findOrderQueryDtos() {

        // ToOne 코드를 한번에 조회
        List<OrderQueryDto> result = findOrders();

        // 루프를 돌면서 컬렉션 추가(추가 쿼리 실행)
        result.forEach(o -> {
            List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
            o.setOrderItems(orderItems);
        });
        return result;
    }

    // 1:N 관계를 제외한 나머지를 한번에 조회
    private List<OrderQueryDto> findOrders() {
        return em.createQuery(
                "select new jpabook.jpashop.repository.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.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();
    }
}

OrderQueryDto

@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;
    }
}

OrderItemQueryDto

@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;
    }
}
  • Query : 루트 1번, 컬렉션 N번 실행
  • ToOne 관계들을 먼저 조회하고, ToMany 관계는 각각 별도로 처리한다.
    • ToOne 관계는 조인해도 데이터 row 수가 증가하지 않는다.
    • ToMany 관계는 조인하면 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() {

    // ToOne 코드를 한번에 조회
    List<OrderQueryDto> result = findOrders();

    // orderItem 컬렉션을 Map 한방에 조회
    Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));

    return result;
}

private List<Long> toOrderIds(List<orderQueryDto> result) {
    return result.stream()
            .map(o -> o.getOrderId())
            .collect(Collectors.toList());
}

private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
    List<OrderItemQueryDto> orderItems = em.createQuery(
            "select new jpabook.jpashop.repository.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();

    return orderItems.stream()
            .collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));

}
  • Query : 루트 1번, 컬렉션 1번
  • ToOne 관계들을 먼저 조회하고, 여기서 얻은 식별자 orderId로 ToMany 관계인 OrderItem을 한꺼번에 조회
  • MAP을 사용해서 매칭 성능 향상


주문 조회 V6

JPA에서 DTO로 직접 조회, 플랫 데이터 최적화

OrderApiController

@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

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;
}

OrderQueryRepositry(+)

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

@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 보다 더 느릴 수도 있다.
    • 애플리케이션에서 추가 작업이 크다.
    • 페이징 불가능


<출처 : 인프런 - 실전! 스프링 부트와 JPA 활용2 (김영한)>

댓글남기기