BackEnd/JPA

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

샤아이인 2022. 4. 20.

내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.

 

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

이번시간에는 Join Fetch를 이용하여 성능 개선을 이루어 낼 것 이다!

 

우선 V3의 메서드는 다음과 같다.

@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
    List<Order> orders = orderRepository.findAllWithMemberDelivery();
    return orders.stream()
            .map(SimpleOrderDto::new)
            .collect(Collectors.toList());
}

새롭게 보이는 findAllWithMemberDelivery() 라는 메서드가 있다.

해당 함수 에서 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();
}

select o , 즉 Order에 대한 모든 정보를 가져올때 fetch join한 member, delivery에 대한 데이터도 한번에 가져오게 된다.

 

이번에는 PostMan을 통해 정보를 요청해 보자! 쿼리가 총 몇번 나가게 될까? (이전 V2에서는 N+1 문제로 인하여 5번 나갔었다)

쿼리가 단 1번 나오게 되었다! 1번의 쿼리로 전부 조회해온 것 이다!

여기서 더 나아가 현재 Select에서 모든 부분 가져오고 있는데, 이 부분까지 성능 개선을 할 수 있다.

 

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

이전 시간에는 Entity로 우선 조회해 온 후, 다시 DTO로 변환하였다.

이번 시간에는 바로 DTO로 조회 해보자!

 

우선 컨트롤러는 다음과 같이 V4 버전으로 변경되었다.

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

위에서 보이는 findOrderDto() 메서드의 코드는 다음과 같다.

 

추가적으로 이번에는 class 안에 static class로 dto를 만드는 것이 아닌, 따로 DTO를 외부에 만들어 주었다.

만약 OrderSimpleQueryDto가 Controller 내부에 있는 static class 였다면, 이를 사용하는 repository 내부에서 controller에 의존성이 생겨버린다. 이는 좋지 못하다!

public List<OrderSimpleQueryDto> findOrderDto() {
    return em.createQuery(
            "select new jpabook.jpashop.repository.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();
}


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

    public OrderSimpleQueryDto(Long id, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}

이번 코드에서는 Join Fetch를 사용하지는 않았다, 다만 new 연산자를 통해 DTO를 Select 절에 명시해주고 있다.

new 명령어를 통해 JPQL의 결과를 DTO로 즉시 반환한다.

다시 postman을 통하여 요청을 보내보자! 결과는 다음과 같다.

select 절에 딱 원하는 부분만 선택하고 있다. v3버전 보다는 조금은 성능 최적화가 된것이다. (생각보다 미비)

다만 이러한 방식은 해당 DTO를 사용할때만 재사용이 가능하다. 다른 경우에서 재사용이 불가능 하다는 단점이 있다.

 

또한 Repository는 원래 Entity에 대한 조회용도로 사용해야 하는데, API 스펙에 맞춰 Repository코드가 작성되어 버렸다.

즉 물리적인 계층이 나눠지기는 했지만, 논리적으로는 API 스펙이 repository까지 침투된것 이다.

repository가 화면에 의존하고 있는 것 이다. API 스펙이 변경되면 repository 내부를 다 수정해야 한다.

 

일반적으로는 Repository에서 Entity를 반환하도록 하는것이 좋을것 이다.

 

아니면, 추가적으로 repository.order.simplequery 라는 디렉토리를 추가로 만든 후, 여기에 해당 DTO용 repository를 따로 만든다.

OrderSimpleQueryRepository를 만들었다.

@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {

    private final EntityManager em;
    public List<OrderSimpleQueryDto> findOrderDto() {
        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();
    }
}

위와 같이 DTO query전용 Repository를 만들수도 있다.

 

쿼리 방식 선택 권장 순서

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

 

이번 시간까지 xToOne 시리즈의 성능 최적화에 대하여 공부하였다.

다음시간부터 xToMany 시리즈의 데이터 뻥티기와 이에대한 성능 최적화를 공부해보자.

댓글