ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 3. API 개발 고급 - 지연 로딩과 조회 성능 최적화
    Spring-Boot/실전! 스프링 부트와 JPA활용2 - API개발과 성능 최적화 2023. 2. 13. 15:06

    1. 간단한 주문 조회 V1 : 엔티티를 직접 노출

    @GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        return all; // 무한루프 - 양방향 연관관계 발생
    }

    1. 무한루프 발생

        ㆍ양방향 연관관계일 경우 한쪽을 @JsonIgnore을 해야한다.

     

    2. bytebuddy.ByteBuddyInterceptor 에러 발생

        ㆍLAZY 지연로딩은 디비에서 값을 가져오는게 아니라 Proxy 객체를 가져온다.

    @Entity
    @Table(name = "orders")
    @Getter @Setter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class Order {
    
        @ManyToOne(fetch = LAZY)
        @JoinColumn(name = "member_id")
        private Member member = new ByteBuddyInterceptor(); // new ProxyMember();
        
    }

        ㆍ이 proxy가 bytebuddy 라는 라이브러리를 쓴다.

        ㆍproxy 객체를(가짜) 넣어두고 Member객체에 접근할려고 하면 디비에 sql을 날린다.

        ㆍ해결방안

        ㆍ1. CPU 스케줄링과 프로세스 관리

              º 1. hibernate5Module 설정

    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
    @Bean
    Hibernate5Module hibernate5Module() {
       Hibernate5Module hibernate5Module = new Hibernate5Module();
       //강제 지연 로딩 설정
       //hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true);
       return hibernate5Module;
    }}

              º 2. Lazy 강제 초기화

    @GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName();         // Lazy 강제 초기화
            order.getDelivery().getAddress();    // Lazy 강제 초기화
        }
        return all; // 무한루프 - 양방향 연관관계 발생
    }

     

    * 참고 : 정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않다.

    - 따라서 Hibernate5Module 를 사용하기 보다는 DTO로 변환해서 반환하는 것이 더 좋은 방법이다.

     

    * 주의 : 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다!

    - 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다.

    - 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 진다.

    - 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용해라

     

    2. 간단한 주문 조회 V2 : 엔티티를 DTO로 변환

    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() {
        // ORDER 2개
        // N + 1 -> 1 + N(회원) + N(배송)
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<SimpleOrderDto> result = orders.stream()
                .map(SimpleOrderDto::new)       // 람다 함수
                .collect(Collectors.toList());
    
        return result;
    }
    
    @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();             //LAZY 초기화
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();     //LAZY 초기화
        }
    }

    - 엔티티를 DTO로 변환하는 일반적인 방법이다.

    - 쿼리가 총 1 + N + N 번 실행된다. ( v1과 쿼리수 결과는 같다.)

        ㆍorder 조회 1번 ( order 조회 결과 수가 N이 된다.)

        ㆍorder -> member 지연 로딩 조회 N 번

        ㆍorder -> delivery 지연 로딩 조회 N 번

        ㆍ예) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다. (최악의 경우)

              º 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.

     

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

    - 엔티티를 페치 조인(fetch join)을 사용해서 쿼리 1번에 조회

    - 페치 조인으로 order → member, order → delivery 는 이미 조회된 상태이므로 지연로딩 X

     

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

    - 일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회

    - new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환 

    - SELECT 절에서 원하는 데이터를 직접 선택하므로 DB → 애플리케이션 네트웍 용량 최적화 ( 생각보다 미비 )

    - 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

     

    정리

    - 엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법은 각각 장단점이 있다.

    - 둘 중 상황에 따라서 더 나은 방법을 선택하면 된다.

    - 엔티티로 조회하면 리포지토리 재사용성도 좋고, 개발도 단순해진다.

     

    * 쿼리 방식 선택 권장 순서 *

    1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.

        ㆍV2

    2. 필요하면 페치 조인으로 성능을 최적화 한다. → 대부분의 성능 이슈가 해결된다.

        ㆍV3

    3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.

        ㆍV4

    4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.

    댓글

Designed by Tistory.