nu_s

[JPA 활용] 컬렉션 조회 최적화 - JPA에서 DTO 직접 조회 🐱 본문

JPA

[JPA 활용] 컬렉션 조회 최적화 - JPA에서 DTO 직접 조회 🐱

woochii 2023. 11. 11. 15:09
728x90
반응형

V4. JPA에서 DTO 직접 조회하기

  • 앞에서는 엔티티를 DTO로 변환해서 컬렉션으로 반환했다.
  • 이번에는 DTO를 그대로 반환해서 JPA가 DTO를 직접 조회하게 만들어 보겠다.
@RestController
@RequiredArgsConstructor
public class OrderApiController {

    private final OrderQueryRepository orderQueryRepository;

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

 

jpabook.jpashop.repository.order.query.OrderQueryDto

 

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

 

jpabook.jpashop.repository.order.query.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;
    }
}
  • JPA에서 직접 조회하는 방식을 사용하기 위해 OrderQueryDto와 OrderItemQueryDto를 생성했다.
  • 기존 Controller에 있으면 Repository가 Controller를 참조해서 의존관계가 순환이 된다.
  • 따라서 패키지를 jpabook.jpashop.repository 안에 새로 생성했다.

 

jpabook.jpashop.repository.order.query.OrderQueryRepository

public List<OrderQueryDto> findOrderQueryDtos() {
    List<OrderQueryDto> result = findOrders();

    result.forEach(o -> {
        List<OrderItemQueryDto> orderItems =  findOrderItems(o.getOrderId());
        o.setOrderItems(orderItems);
    });

    return result;
}
  • findOrderQueryDtos 에서는 Order를 조회하고 DTO로 바꿔주고 Order의 수만큼 OrderItem도 DTO로 변환해준다.
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();
}
  • findOrders에서는 ToOne 관계인 member, delivery를 fetch join으로 한번에 가져온다.
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();
}
  • findOrderItems 에서는 orderItem에서 item을 fetch 조인으로 가져온다.
  • item은 Order와는 XXXToMany 관계이지만, orderItem과는 XXXToOne 관계이므로 페치조인이 가능하다.

 

 쿼리 실행 수

  • findOrderQueryDtos()를 통해 실행되는 쿼리의 수를 세보자
  • findOrders()에서 Order, Member, Delivery를 조인하여 불러오는 쿼리 1번
  • findOrderItems()에서 OrderItem, Item을 조인하여 불러오는 쿼리 N번
  • 현재 DB에 주문이 2건 있으므로 1 + 2 = 3  총 3번의 쿼리가 실행된다.
  • 이번에도 N + 1 문제가 발생한 것을 볼 수 있다.
  • 이 방법 또한 최적화가 필요하다.

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

@RestController
@RequiredArgsConstructor
public class OrderApiController {

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

 

findAllByDto_optimization()

public List<OrderQueryDto> findAllByDto_optimization() {
    //모든 주문 조회
    List<OrderQueryDto> result = findOrders();

    //모든 주문의 주문번호를 List로 가져온다.
    List<Long> orderIds = result.stream()
            .map(o -> o.getOrderId())
            .collect(Collectors.toList());

    //orderIds로 모든 OrderItem을 조회한다.
    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();

    //groupingby를 통해 Map으로 변환한다.
    Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
            .collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
    
    result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));

    return result;
}

 

V4와의 차이점

  • findAllByDto_optimization() 내에 작성된 쿼리를 보면 where절에 '='가 아니라 'in'이 들어가있다.
  • orderId에 해당하는 데이터를 하나씩 쿼리로 실행하는 것이 아니라, 모든 orderId를 조회해서 그에 해당하는 주문이 있다면 다 가져오는 것이다.

 

정리

  • 쿼리는 루트 1번, 컬렉션 1번 -> 총 2번 실행된다.
  • XXXToOne 관계를 먼저 조회하고, 식별자 orderId를 얻어 XXXToMany 관계인 OrderItem을 한꺼번에 조회한다.
  • Map을 사용해서 매칭 성능을 향상시켰다.

출처 : 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

728x90
반응형