본문 바로가기
ORM/JPA

[JPA] 지연 로딩과 조회 성능 최적화

by 젊은오리 2023. 4. 16.
728x90

[김영한님의 실전! 스프링 부트와 JPA 활용2 강의 학습 후 정리한 내용입니다.]

서론

  • xToOne(ManyToOne, OneToOne)관계에서 성능 최적화를 해보자.
  • 여기에서는 Orders를 조회하는 API를 예시로 든다.
  • 참고로, Order와 Member가 ManyToOne관계이며, Delivery와는 OneToOne관계이다.

순서

  1. 주문 조회 V2 (엔티티를 DTO로 변환)
  2. 주문 조회 V3 (엔티티를 DTO로 변환 - Fetch join 최적화)
  3. 주문 조회 V4 (JPA에서 DTO로 바로 조회)

 

1. 주문조회 V2(엔티티를 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번 실행된다. (orders 뽑아내는 쿼리 1개 + order.member 지연 로딩 조회 N번 + order.delivery 지연 로딩 조회 N번) 만약 orders의 결과가 3개일 경우에는 총 1 + 3 + 3 = 7번의 쿼리가 실행된다.

 

2. 주문조회 V3(엔티티를 DTO로 변환 - fetch join 최적화)

  • V2와 같지만, orders를 뽑아내는 쿼리문이 다르다. 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을 사용해서 orders를 불러올 때 연관된 member 테이블과 delivery 테이블을 한번에 묶어서 불러온다.

[OrderRepository]

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

 

Repository는 순수 엔티티를 갖고 동작하는 쿼리를 작성하는 것이 좋다. 따라서 별도의 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을 사용할 때 처럼 원하는 값을 선택해서 조회할 수 있다. new 명령어를 사용해서 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과 join하는 부분은 동일하지만, 원하는 테이블의 속성을 갖고 온다는 차이가 있다. (SELECT절에서 원하는 데이터를 직접 선택한다.)

 

정리

엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법은 각각 장단점이 있다. 따라서 둘중 상황에 따라 더 나은 방법을 선택하면 된다. 쿼리 방식 선택 권장 순서는 다음과 같다.

  1. 엔티티를 DTO로 변환하는 방법을 선택한다.(V2)
  2. 필요하면 fetch join으로 성능을 최적화 한다. <- 여기서 대부분의 성능 이슈가 해결된다.(V3)
  3. DTO로 직접 조회하는 방법을 사용한다.(V4)
  4. JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template SQL을 사용한다.
728x90

'ORM > JPA' 카테고리의 다른 글

[JPA] OSIV 성능 최적화 정리  (0) 2023.04.18
[JPA] 컬렉션 조회 최적화  (0) 2023.04.17
[JPA] 더티 체킹(dirty checking) 정리  (0) 2023.04.16
[JPA] JPA Auditing 정리 및 구현  (0) 2023.04.10
[JPA] 연관관계 매핑 참고  (0) 2023.01.03

댓글