본문 바로가기

카테고리 없음

API 개발 고급 -지연로딩과 조회 성능 최적화

728x90

주문 , 배송정보 , 회원을 조회하는 API를 여러방법으로 만들어봅니다.그러면서 지연로딩 때문에 발생하는 성능문제를 단계적으로 해결해보자.

 

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

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Order;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;


/**
 * xToOne(ManyToOne, OneToOne
 * Order
 * Order -> Member
 * Order -> Delivery
 */
@RequiredArgsConstructor
@RestController
public class OrderSimpleApiController {
    private final OrderRepository orderRepository;

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

 

 

엔티르르 직접 노출하는것은 좋지않다!!

해당 API를 호출하면 Order<->Member와 Order<->delivery간 엔티티를 조회하는것이 무한순회하면서 

에러가 발생하게 된다.

@JsonIgnore

양방향 연관관계부분에 @JsonIgnore Annotation을 추가해주면 무한 루프 에러는 해결된다, 하지만 다음과 같은 에러가 발생한다.
→ 현재 코드의 로딩 전략은 지연로딩(LAZY)전략을 사용한다. 그 말은 Member, Delivery등의 엔티티들을 조회하는 시점에서는 실제 객체가 아닌 프록시 객체를 가지고 있다. 그렇기 때문에 jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모르기 때문에 예외가 발생하는 것이다.

 

해결책

-> Hibernate5Module 을 스프링 Bean으로 등록하면 해결된다.

/*build.gradle*/
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

/*JpashopApplication.java*/
@Bean
Hibernate5Module hibernate5Module(){
	return new Hibernate5Module();
}

gradle implentation 할때 version을 명시해주지않으면 gradle에서 자동으로 현재버전과 잘맞는 버젼을import시켜준다.

 

(Important) 계속 언급했지만 엔티티를 API에 직접 노출하는 것은 좋지 않다. 그래서 Hibernate5Module을 이용하는 방법은 알아만 두고 실무에서는 DTO로 변환해서 반환하는것이 더 좋은 방법이다

 

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

 

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;


/**
 * xToOne(ManyToOne, OneToOne
 * Order
 * Order -> Member
 * Order -> Delivery
 */
@RequiredArgsConstructor
@RestController
public class OrderSimpleApiController {
    private final OrderRepository orderRepository;

    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2(){
        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(); //LAZY 초기화
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress(); //LAZY 초기화
        }
    }
}

V2 버젼으로 API호출했을 때 실행되는 문제.

 

1+N+N번 실행된것을 확인할수있다.

 

Order 조회가 많아질수록 기하급수적으로 쿼리수행이 많아지게되고,성능저하게 일어날수있다.

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

 

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;


/**
 * xToOne(ManyToOne, OneToOne
 * Order
 * Order -> Member
 * Order -> Delivery
 */
@RequiredArgsConstructor
@RestController
public class OrderSimpleApiController {
    private final OrderRepository orderRepository;

    @GetMapping("/api/v3/simple-orders")
    public List<SimpleOrderDto> ordersV3(){
        List<Order> orders = orderRepository.findAllWithMemberDelivery();

        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(); //LAZY 초기화
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress(); //LAZY 초기화
        }
    }
}
...
/*orderRepository.findAllWithMemberDelivery*/
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();
}

엔티티를 fetch join을 사용해서 쿼리를 1번에 조회패치조인으로 order ->member, order->delivery 는 이미조회 된 상태이므로 지연로딩x

 

 

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

 

/**
 * V4. JPA에서 DTO로 바로 조회
 * -쿼리1번 호출
 * - select 절에서 원하는 데이터만 선택해서 조회 
 */
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
	return orderSimpleQueryRepository.findOrderDtos(); 
}

 

 

결론:

엔티티를 DTO로 변환하거나, DTO로 바호 조회하는 두가지방법은 각각 장단점이 있다. 둘 중 상황에 따라서 더 나은

방법을 선택하면된다.

엔티티로 조회하면 리포지토리 재사용성도 좋고,개발도 단순해진다, 따라서 권장하는 방법은 다음과 같다.

 

쿼리 방식 선택 권장 순서

 

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

2. 필요하면 Fetch join으로 성능을 최적화한다. ->대부분의 이슈는 해결될것이다.

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

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

728x90