ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 간단한 주문조회 API - 지연 로딩과 조회 성능 최적화(2)
    JPA/JPA 활용 2023. 7. 11. 18:16

    저번 포스팅에서 Order 조회 버전 1과 2를 했는데, 1에서는 엔티티를 그대로 사용해서 문제점이 있었고, 2에서는 Lazy 로딩때문에 쿼리가 너무 많이 호출되는 문제점이 있었다.

     

     

    이번에 Version 3에서는 fetch join으로 성능 최적화를 진행할 것이다.

     

     

    Version3

    orderRepository에 finAllWithMemberDelivery() 메서드 추가

    public List<Order> findAllWithMemberDelivery() {
        return em.createQuery(
                "select o from Order o" +
                        " join fetch o.member m" + // order 가져올 때 멤버까지 한방 쿼리로 가져오는 'join fetch'
                        " join fetch o.delivery d", Order.class
        ).getResultList();
    }

    Order를 조회할 때 member랑 delivery를 join fetch로 한 번에 가져온다.

    엔티티에는 Lazy로 설정되어 있지만 이 메서드에서 join fetch를 사용해서 한 번의 쿼리로 가져오는 것이다.

     

    @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;
    }

    Version2와 컨트롤러는 비슷한데 나가는 쿼리가 다르다. orderRepository에서 findAllWithMemberDelivery()를 호출했기 때문이다. 이전과 차이점은 N+1문제가 해결되었고, 쿼리 1번에 다 끝난다는 것이다. member와 delivery가 order 쿼리에 포함되어서 하나의 쿼리로 나온다.

    인텔리제이 콘솔 창에 뜨는 쿼리문

    실무에서 JPA를 사용할 때에 성능 문제의 대부분이 fetch join 문제이고, JPA를 사용하려면 이 개념을 확실히 알아둬야 한다. 기본으로 LAZY로 설정을 해 둔다음 필요할 때에만 fetch join으로 쿼리를 만들어주면 성능에서 확실한 이점을 가져올 수 있다.

     

     

     

     

    Version 4

    orderRepository.findOrderDtos

    public List<OrderSimpleQueryDto> findOrderDtos() {
        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();
    }

    마지막 버전은 엔티티를 Dto로 바꿔왔던 이전 버전과 다르게 그냥 Dto를 받아와서 반환하는 것이다.

    select문에 new 명령어를 사용해서 JPQL의 결과를 Dto로 즉시 변환하는 것이 목적이다.

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

    컨트롤러에서도 보면 그냥 repository의 메서드를 호출해서 반환하는 것이다.

     

    Version 4의 쿼리인데, Version 3의 쿼리보다 select문이 줄어든 것을 확인할 수 있다.

     

     

     

     

    하지만 Version 3보다 Version 4가 무조건 좋은 것은 아니다. 각각의 장단점이 있기 때문이다.

    Version 4는 API를 repository에서 사용하기 때문에 객체 설계 관점에서 볼 때, repository가 API를 사용하는 것이다. repository는 엔티티를 조회해야하는데, API 스펙을 조회하는 것이다.  하지만 성능 상으로는 확실히 Version 4가 좋다.

     

     

    성능 테스트를 통해서 장단점을 비교하면서 최적의 버전을 사용하는 것이 좋지만 join을 하거나 where 문에서 성능 차이가 크고, Version 3과 4의 차이점인 필드의 개수는 그렇게 크게 성능 차이가 나지 않아서 Version 3을 사용하는 것이 더 좋다고 생각한다.

     

     

    특수한 상황

    ex) 고객이 계속 들어오는 트래픽이 많은 경우는 Version 4를 사용하는 것이 좋다고 생각한다.

    Version 4를 사용해야하는 경우는 repository가 아니라 별도의 Order.query repository라는 최적화 전용 조회 repository로 분리해서 사용하는 것이 좋다.

     

     

     

    만약에 이래도 성능 최적화가 해결이 안된다면 최후의 방법으로 JPA가 제공하는 네이티브 SQL이나 JDBC Template을 사용해서 SQL을 직접 사용한다.

     

     

     

    지금까지는 @ManyToOne, @OneToOne에서의 최적화였고 다음부터는 컬렉션 조회 최적화를 진행할 것이다.

Designed by Tistory.