ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA(2) - 지연 로딩과 조회 성능 최적화
    카테고리 없음 2024. 1. 22. 11:31

     

     

    1. 엔티티 직접 노출 

     

     private final OrderRepository orderRepository;
     /**
     * V1. 엔티티 직접 노출
     * - Hibernate5Module 모듈 등록, LAZY=null 처리
     * - 양방향 관계 문제 발생 -> @JsonIgnore
     */
     @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;
     }

     

    - 일단 엔티티 직접 노출부터 문제

    - 가져온 것을 바로 리턴하면 무한 루프 문제 발생합니다.

       

       해결법1: @JsonIgnore

     Order와 관련된 모든 Lazy로딩 (프록시 객체)에 @JsonIgnore를 사용해야함

     양방향은 한쪽에 JsonIgnore를 붙여준다. 

    	//Order
        @JsonIgnore
        @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
        private List<OrderItem> orderItems = new ArrayList<>();
    
        @JsonIgnore
        @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
        @JoinColumn(name = "delivery_id")
        private Delivery delivery;
        
        //Member
        @JsonIgnore
        @OneToMany(mappedBy = "member")
        private List<Order> orders = new ArrayList<>();
        
        //orderItem
        @JsonIgnore
        @ManyToOne(fetch = LAZY)
        @JoinColumn(name = "order_id")
        private Order order;
        //..

     

    해결법2: Hibernate5JakartaModule 등록 -> 지연로딩일 경우 response를 제외시켜준다.

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

     

    - 초기화된 프록시 객체만 노출 초기화 안된건 노출 안함 

    - Json으로 만들 때 강제 지연로딩 설정 : 다 불러와서 초기화하라고 설정하는 것 

     Hibernate5Module hibernate5Module = new Hibernate5Module();
     //강제 지연 로딩 설정
     hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, 
    true);
     return hibernate5Module;

     

    해결법 3: 리턴전에 Lazy강제 초기화 -> N+1문제 발생 (애플리케이션 레벨에서 지연로딩 풀어주기)

     

     - 물론 이 옵션을 켜면 order -> member, member -> orders 양방향 연관관계를 계속 로딩해야함 따라서 @JsonIgnore를 한 곳에 주어야함 (엔티티 직접 노출 + 양방향 관계에선 반드시 한쪽에 @JsonIgnore 걸어야함)

     

    - em.find는 성능과 별개 (어차피 식별자로 불러오는거라) 즉시로딩은 jpql에서 성능문제 발생 (연관관계만큼 select)

    - 항상 지연로딩 -> 성능 최적화 필요시 페치 조인을 사용 

     

     * 3가지 해결법 모두 구린 방법! 결국 엔티티를 그냥 리턴하기위한 궁리일 뿐이다.

     

    2. 엔티티 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(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();
     orderDate = order.getOrderDate();
     orderStatus = order.getStatus();
     address = order.getDelivery().getAddress();
     }
    }

     

     - 엔티티를 DTO로 바꾸는 일반적인 방법임 

     - 하지만 지연로딩때문에 1 + N + N 실행 

         -> order 1번조회 (member 지연로딩 2개, 주소 지연로딩 2개) 

         -> 두 지연로딩을 풀기위해 추가로 select 4번 나감 

     

     

    3. DTO 변환 + 페치조인 최적화 

     

    @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(toList());
     return result;
    }
    
    
    //repository
    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번만 나가서 엔티티를 채웠다! (이미 조회 상태이므로 지연로딩 x)

     

    4. 한발 더 나아가서 DTO로 직접 조회하기 

    @GetMapping("/api/v4/simple-orders")
    public List<OrderSimpleQueryDto> ordersV4() {
     return orderSimpleQueryRepository.findOrderDtos();
    }
    
    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();
     }

     

     - value object나 Entity 제외하고, jqpl의 리턴값을 만드려면 new 연산자를 사용해줘야함 (queryDSL에서 이를 해결한다함)

    - DTO를 바로 만들어서 리턴하면, 애플리케이션 레벨에서 변환 로직이 빠지고, 조회결과 데이터 수가 줄어서 성능 최적화를 노려볼 수 있음 

     - 하지만 리포지토리 재사용성이 떨어지고, API 스펙에 맞춘 코드가 리포지토리에 들어감 (상충관계있음)

     

     

    * 권장 순서 

      엔티티 -> DTO는 무조건 지켜야한다.

      엔티티에 지연로딩이 걸려있어 성능 최적화 필요시 페치조인을 사용한다.

      더 나아가 성능을 최적화하고 싶을 때만 DTO직접조회 방식을 사용해본다.

      최후의 방법은 네이티브 SQL 혹은 JDBC Template를 사용해서 직접 SQL을 사용한다. 

Designed by Tistory.