간단한 주문 조회 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

간단한 주문 조회 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 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

ordersV3 vs ordersV4
V3
재사용성이 좋고, 코드 상 더 간단하게 짤 수 있다.
필요한 DTO를 만들어서 활용하기에 유리하 범용성이 높다.
V4
원하는 데이터만 골라서 가져와서 네트워크 사용이 작다.
재사용성이 떨어져 범용성이 떨어진다.
쿼리 방식 선택 권장 순서