[스프링과 JPA활용 2편] 요약 정리6

sonnng·2023년 11월 4일
0

Spring

목록 보기
28/41

주문 조회 V4: JPA에서 DTO 직접 조회

api controller

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

query repository

	public List<OrderQueryDto> findOrderQueryDtos(){
        List<OrderQueryDto> result = findOrders(); //query 1번 -> n개

        //결론적으로 쿼리 n+1번
        result.forEach(o -> {
            List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
            o.setOrderItems(orderItems);
        });
        return result;
    }
    
    public 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();
    }

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

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

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

위 코드의 V4의 문제는 Query 루트 1번이 실행이 되고 추가적으로 컬렉션을 도는 코드로 인해 쿼리가 N번이 실행된다는 점이다.

  • ToOne 관계들을 먼저 조회(join)하고 ToMany관계들은 각각 별도로 처리한 방법이다.

이런 방식을 택한 이유는, ToOne관계는 조인을 해도 데이터 row수가 증가하지 않지만 ToMany관계는 조인시 ROW 수가 증가하기 때문이다.

따라서 ToOne관계는 조인으로 최적화하기 쉬우므로 한번에 조회하고, ToMany관계는 최적화하기 어려우므로 findOrderItems()같은 별도의 메서드를 사용하는 것이다.



주문 조회 V5: JPA에서 DTO 직접 조회 - 컬렉션조회 최적화

API controller

@GetMapping("/api/v5/orders")
public List<OrderQueryDto> ordersV5() {
return orderQueryRepository.findAllByDto_optimization(); 
}

query repository

/**
 * 최적화
 * Query: 루트 1번, 컬렉션 1번
 * 데이터를 한꺼번에 처리할 때 많이 사용하는 방식 
 *
 */
public List<OrderQueryDto> findAllByDto_optimization() {
  //루트 조회(toOne 코드를 모두 한번에 조회)
      List<OrderQueryDto> result = findOrders();
  //orderItem 컬렉션을 MAP 한방에 조회
      Map<Long, List<OrderItemQueryDto>> orderItemMap = 
  findOrderItemMap(toOrderIds(result));//루프를 돌면서 컬렉션 추가(추가 쿼리 실행X)
      result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
  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.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();
  return orderItems.stream()
  .collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
}

위 코드를 통해 v5의 쿼리는 루트 1번, 컬렉션 1번 조회로 컬렉션 조회가 최적화되었다.

  • ToOne관계를 먼저 조회(join)하고 여기서 얻은 식별자 orderId로 ToMany관계인 OrderItem을 한꺼번에 조회해 올 수 있다.

  • Map을 통하여 매칭 성능을 향상하게 된다.(o(1))



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

api controller

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

query Dto 설정추가

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

query repository

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

flat dto

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

v6를 통해 쿼리는 1번만 실행되는 장점을 보여준다.

😱 단점

  • 쿼리는 한번이나, 조인이 여러개 묶여있어서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 v5보다 느릴 수 있다.

  • 애플리케이션에 추가작업이 크며, 페이징이 불가능하다.

0개의 댓글