프로그래밍/spring boot

[스프링부트] 실전! 스프링 부트와 JPA 활용2 컬렉션 조회 최적화 #4 JPA에서 DTO 직접 조회

aSpring 2023. 11. 26. 16:13
728x90
728x90
※ 본 포스팅은 김영한 강사님의 인프런 '실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화' 강의를 들으며 작성한 수강일지 입니다.

 
 

 

 

| API 개발 고급 - 컬렉션 조회 최적화

1. 주문 조회 V1: 엔티티 직접 노출
2. 주문 조회 V2: 엔티티를 DTO로 변환
3. 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화
4. 주문 조회 V3.1: 엔티티를 DTO로 변환 - 페이징과 한계 돌파
5. 주문 조회 V4: JPA에서 DTO 직접 조회
6. 주문 조회 V5: JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화 
7. 주문 조회 V6: JPA에서 DTO로 직접 조회, 플랫 데이터 최적화
8. API 개발 고급 정리

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

 

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

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

 

OrderApiController에 추가

// v4 - JPA에서 DTO 직접 조회
@GetMapping("/api/v4/orders")
public List<OrderQueryDto> ordersV4() {
    return orderQueryRepository.findOrderQueryDtos();
}

//    @Data
@Getter
static class OrderDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate; //주문시간
    private OrderStatus orderStatus;
    private Address address; // Entity를 다 노출하면 안됨, but Address 같은 value object는 상관없음
    private List<OrderItemDto> orderItems;

    // 생성자
    public OrderDto(Order order) {
        orderId = order.getId();
        name = order.getMember().getName();
        orderDate = order.getOrderDate();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress();
        orderItems = order.getOrderItems().stream() // loop로 돌리면서
//                    .map(OrderItemDto::new)
                .map(orderItem -> new OrderItemDto(orderItem)) // 생성자를 통해 dto로 변환
                .collect(Collectors.toList());
    }
}

 

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

 

 

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

 

 

  • Query: 루트 1번, 컬렉션 N 번 실행
  • ToOne(N:1, 1:1) 관계들을 먼저 조회하고, ToMany(1:N) 관계는 각각 별도로 처리한다.
    • 이런 방식을 선택한 이유는 다음과 같다.
    • ToOne 관계는 조인해도 데이터 row 수가 증가하지 않는다.
    • ToMany(1:N) 관계는 조인하면 row 수가 증가한다.
  • row 수가 증가하지 않는 ToOne 관계는 조인으로 최적화 하기 쉬우므로 한번에 조회하고, ToMany 관계 는 최적화 하기 어려우므로 findOrderItems() 같은 별도의 메서드로 조회한다.

 

 

728x90
728x90