간단한 주문 조회 V1: 엔티티를 직접 노출

Member

@JsonIgnore //양방향 연관관계에서 무한루프에 빠지지 않도록 함.
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();

Delivery

@JsonIgnore //양방향 연관관계에서 무한루프에 빠지지 않도록 함.
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
private Order order;

OrderItem

@JsonIgnore //양방향 연관관계에서 무한루프에 빠지지 않도록 함.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;    //주문

엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한곳을 @JsonIgnore 처리 해야 한다. 안그러면 양쪽을 서로 호출하면서 무한 루프가 걸린다.

OrderSimpleApiController

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    @GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1(){
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName();    //Lazy 강제 초기화
            order.getDelivery().getAddress();   //Lazy 강제 초기화
        }
        return all;
    }
}

간단한 주문 조회 V2: 엔티티를 DTO로 변환

@GetMapping("/api/v2/simple-orders")    //v1,v2 모두 lazy로딩으로 인해 query가 많이 호출된다.
public List<SimpleOrderDto> ordersV2(){ // n + 1 법칙에 의해 query가 5번 호출됨. 1 + 배송(2) + 회원(2)

    List<Order> orders = orderRepository.findAllByString(new OrderSearch());
    List<SimpleOrderDto> result = orders.stream()
            .map(o -> new SimpleOrderDto(o))
            .collect(Collectors.toList());

    return result;
}

@Data
static class SimpleOrderDto {
    private Long orderId;
    private String name;
    private LocalDateTime localDateTime;
    private OrderStatus orderStatus;
    private Address address;

    public SimpleOrderDto(Order order) {
        this.orderId = order.getId();
        this.name=order.getMember().getName();
        this.address=order.getDelivery().getAddress();
        this.orderStatus = order.getStatus();
        this.localDateTime = order.getOrderDate();
    }
}

엔티티를 DTO로 변환하는 일반적인 방법이다.

쿼리가 총 1 + N + N번 실행된다. (v1과 쿼리수 결과는 같다.)

order 조회 1번(order 조회 결과 수가 N이 된다.)

order -> member 지연 로딩 조회 N 번

order -> delivery 지연 로딩 조회 N 번

예) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다.(최악의 경우)

지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.

간단한 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화

@GetMapping("/api/v3/simple-orders")
    public List<SimpleOrderDto> ordersV3(){
        List<Order> orders = orderRepository.findAllWithMemberDelivery();
        List<SimpleOrderDto> result = orders.stream()
                .map(SimpleOrderDto::new)   //V2와 같은 코드.
                .toList();
        return result;
    }

OrderRepository에 추가

public List<Order> findAllWithMemberDelivery(){ //ordersV3에 사용.
        return em.createQuery(
                "select o from Order o" +
                        " join fetch o.member m" +
                        " join fetch o.delivery d", Order.class)
                .getResultList();
    }

엔티티를 페치 조인(fetch join)을 사용해서 쿼리 1번에 조회

페치 조인으로 order -> member , order -> delivery 는 이미 조회 된 상태 이므로 지연로딩X

Untitled

간단한 주문 조회 V4: JPA에서 DTO로 바로 조회

private final OrderSimpleQueryRepository orderSimpleQueryRepository;

@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4(){
    return orderSimpleQueryRepository.findOrderDtos();
}

OrderSimpleQueryRepository

@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {

    private final EntityManager em;

    public List<OrderSimpleQueryDto> findOrderDtos(){
        return em.createQuery(
                "select  new jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto(o.id, m.name,o.orderDate,o.status,d.address)" +
                        " from Order o" +
                        " join o.member m" +
                        " join o.delivery d", OrderSimpleQueryDto.class)
                .getResultList();
    }
}

OrderSimpleQueryDto

@Data
public class OrderSimpleQueryDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    public OrderSimpleQueryDto(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;
    }
}

일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회

new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환

SELECT 절에서 원하는 데이터를 직접 선택하므로 DB → 애플리케이션 네트워크 용량 최적화(생각보다 미비)

리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

Untitled

ordersV3 vs ordersV4

V3

재사용성이 좋고, 코드 상 더 간단하게 짤 수 있다.

필요한 DTO를 만들어서 활용하기에 유리하 범용성이 높다.

V4

원하는 데이터만 골라서 가져와서 네트워크 사용이 작다.

재사용성이 떨어져 범용성이 떨어진다.

쿼리 방식 선택 권장 순서

  1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
  2. 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.
  3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
  4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.