(김영한의 실습! Spring Boot와 JPA Utilization 2 강의를 듣고 정리한 내용입니다.)
소개
- xToOne(ManyToOne, OneToOne) 관계에서 성능을 최적화하자.
- 여기에서는 주문을 검색하는 API를 예로 들었습니다.
- 참고로 Order와 Member는 ManyToOne 관계이고 Delivery는 OneToOne 관계입니다.
색인
- 주문 조회 V2(엔티티를 DTO로 변환)
- 주문 조회 V3(엔티티를 DTO로 변환 – 조인 최적화 가져오기)
- 주문 조회 V4(JPA에서 DTO로 직접 조회)
1. 주문 조회 V2(Entity를 DTO로 변환)
- 이는 엔터티를 DTO로 변환하는 일반적인 방법입니다.
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2(){
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return result;
}
쿼리는 총 1 + N + N 번 실행됩니다. (주문 추출 쿼리 1회 + order.member 지연 로딩 조회 N회 + 주문.배송 Lazy loading query N times) 주문이 3개라면 총 1 + 3 + 3 = 7개의 query를 수행한다.
2. 주문 조회 V3(엔티티를 DTO로 변환 – 조인 최적화 가져오기)
V2와 동일하지만 주문 추출을 위한 쿼리문이 다릅니다. findAllwithMemberDelivery를 재정의합시다.
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return result;
}
아래와 같이 Fetch Join을 이용하여 오더를 가져오면 연결된 멤버 테이블과 배송 테이블이 한번에 묶음으로 가져옵니다.
(주문 저장소)
public List<Order> findAllWithMemberDelivery() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.getResultList();
}
결과적으로 클라이언트는 아래와 같이 질의문 1로 응답할 수 있다.

3. 주문 조회 V4(JPA에서 DTO로 직접 조회)
아래와 같이 클라이언트에 응답하기 위해 Repository에서 Dto를 반환하는 쿼리가 생성됩니다.
응답을 위해 OrderSimpleQueryDto 및 findOrderDtos() 메서드를 생성해야 합니다.
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderSimpleQueryRepository.findOrderDtos();
}
리포지토리는 순수 엔터티로 작동하는 쿼리를 작성하는 데 적합합니다.
따라서 쿼리를 작성하기 위해 별도의 OrderSimpleQueryRepository가 생성됩니다.
(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();
}
}
일반 SQL을 사용하는 경우와 같이 원하는 값을 선택하여 조회할 수 있습니다.
JPQL 결과는 새 명령을 사용하여 OrderSimpleQueryDto로 직접 변환되었습니다.
(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;
}
}
결과는 아래와 같이 V3로 조인한 부분과 동일하지만 원하는 테이블의 속성을 가져오는 부분에서 차이가 있습니다. (SELECT 절에서 원하는 데이터를 직접 선택합니다.)

정리하다
- 엔터티를 DTO로 변환하거나 DTO로 직접 쿼리하는 것에는 장단점이 있습니다.
- 둘 중 어느 것이 더 나은지는 상황에 따라 다릅니다.
- 엔터티별 쿼리는 리포지토리 재사용성을 개선하고 개발을 간소화합니다.
질의 방법을 선택하는 권장 순서는 다음과 같습니다.
- 엔터티를 DTO로 변환하는 방법을 선택합니다.(V2)
- 필요한 경우 페치 조인으로 성능을 최적화합니다. <- 이것은 대부분의 성능 문제를 해결합니다.(V3)
- DTO에 직접 조회하는 방법을 사용합니다.(V4)
- JPA에서 제공하는 Native SQL이나 Spring JDBC Template SQL을 사용합니다.