지연로딩 성능 최적화

강한친구·2022년 7월 18일
0

JPA

목록 보기
13/27

Order 조회

지금까지 만들던 서비스에서 조회용 API를 만들어보자.

    @GetMapping("/api/v1/simple-order")
    public List<Order> orderV1() {
        List<Order> allByString = orderRepository.findAllByString(new OrderSearch());
        return allByString;
    }

JPA를 통해서 만든 프로젝트에서 다음과 같은 query로 전체 order를 조회하면 다음과 같은 결과가 나온다.

이는 LazyLoading 관련 오류이다.

Order 객체를 호출 할떄, Member 객체는 Lazy 로딩이기때문에 호출되지 않고, hibernate가 byteBuddy라는 Proxy객체를 이용해서 처리한다. 이 과정에서 오류가 발생하는것이다.

Hibernate5

	implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

위의 라이브러리를 이용하면 ForceLazyLoading 기능을 사용할 수 있고, 이 기능을 이용하면 필요한 정보를 전부 강제로딩해서 처리할 수 있다.

하지만 이는 엔티티를 전부 노출하는 기능이고, 성능상으로도 문제가 생긴다.

또한 이는 API 스펙이 너무 복잡해지는 문제가 있다.

DTO로 전송

    @GetMapping("/api/v1/simple-order")
    public List<SimpleOrderDto> orderV1() {
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<SimpleOrderDto> collect = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());
        return collect;
    }
    
    @Data
    static class SimpleOrderDto {
        private long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;

        public SimpleOrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();

        }
    }

이런식으로 출납용 DTO를 따로 만들어서 엔티티를 노출시키지 않고, order를 가지고 온 다음, order.getMember() 메서드를 호출하는 두번째 과정을 통해서 LAZY를 초기화 할 수 있다.

쿼리는 Order -> Member -> Delivery 순으로 총 3회 조회가 나가게 된다.

하지만, 실제로 쿼리를 돌려보면 다음과 같이 나온다


new 를 통해 새로운 DTO를 계속 만들기에, 이를 반복실행하기위해 Member, Delivery 조회 쿼리가 계속 나가게 된다. 이를 N+1 문제라고 한다.

쿼리가 총 1 + N + N번 실행된다. (v1과 쿼리수 결과는 같다.)
order 조회 1번(order 조회 결과 수가 N이 된다.)
order -> member 지연 로딩 조회 N 번
order -> delivery 지연 로딩 조회 N 번
예) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다.(최악의 경우)
지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다

fetch join

lazy를 전부 무시하고 진짜 객체의 값을 전부 채운다음 한번에 끌고오는 기능을 사용할 수 있다.

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

이런 쿼리문을 실행하면 된다.

보이는것처럼 전부 조인을 한 후, 한번의 query로 찾아와서 정리한다.

JPA에서 DTO 바로 검색

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

이런 복잡한 query문을 추가하면 바로 조회가 가능하다.
(JPA 강의 참고)

내가 원하는 컬럼만 select 했기때문에 좀 더 짧은 쿼리가 나가게 된다.

조인부분까지는 똑같지만, fetch 부분에서 달라지는것이다.

fetch보다는 약간의 성능향상은 있지만, fetch의 경우 entity를 들고오기때문에 자료를 가공하는것이 가능하지만, DTO 검색의 경우 가공이 불가능하다.

또한 코드가 좀 지저분하게 나온다.

대부분의 경우에서는 두 방식이 성능차이가 나지 않기때문에 fetch를 쓰는게 낫다.

김영한님의 권장사항
쿼리 방식 선택 권장 순서
1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
2. 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.
3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접
사용한다.

0개의 댓글