JAVA

[JAVA] 지연로딩과 조회 성능 최적화

응디 2022. 5. 19. 16:20

주문 + 배송정보 + 회원을 조회하는 API를 만들자

지연 로딩때문에 발생하는 성능 문제를 단계적으로 해결해보자.

 

1. 엔티티를 직접 노출 (실무에서는 엔티티를 노출하는 일은 없으니 가볍게 보고 넘기기로 하자.)

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

위에 코드는 엔티티를 직접 노출하는 개발 방식

양방향 연관관계일때 문제가 생김 → 무한루프에 빠져버림 (잘못된 코드)

이러한 경우 양방향이 걸리는데를 다 @JsonIgnore 를 걸어줘야함

→ 그렇다면 무한 루프가 생기는 일은 피할 수 있지만 지연로딩 관련한 문제는 해결되지 않는다.

 

지연로딩으로 인한 프록시 문제는 Hibernate5Module bean등록으로 막을 수 있다.

maven repository에서 라이브러리를 찾아 build.gradle에 추가 후 Bean을 등록하자!

@Bean
Hibernate5Module hibernate5Module(){
   return new Hibernate5Module();
}

이러한 문제를 피하려고 지연 로딩(LAZY)를 즉시로딩(EAGER) 으로 설정하면 안된다!

즉시 로딩으로 인해서 다른 API 사용시 성능이 저하 될 수 있고 즉시 로딩으로 설정하면

나중에 성능 문제를 해결하기가 매우 어려워 진다.

 

항상 지연로딩을 기본으로, 성능 최적화가 필요한 경우에는 fetch join을 사용해라!

이러한 문제들도 해결할 수 있지만 애초에 엔티티를 API 외부로 노출하는 것은 좋지 않다.

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

 

아래는 DTO로 변환하는 예제이다.


2. 엔티티를 DTO로 변환( fetch join 사용X )

/**
* V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용 X)
* 단점 : 지연로딩으로 쿼리 N번 호출
*/

@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2(){
        List<SimpleOrderDto> orders = orderRepository.findAll()
                .stream().map(o -> new SimpleOrderDto(o)).collect(Collectors.toList());
        return orders;
        
        
        // 줄여서 이렇게 사용가능
        /*return orderRepository.findAll()
                .stream().map(SimpleOrderDto::new).collect(Collectors.toList());*/
}

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

문제점 : 이렇게 사용하면 실행은 잘 되지만 쿼리가 1+N+N번 실행이 된다.

order 갯수에 따라 N이 바뀐다. → order 하나당 주문자와 주문 주소가 다 다르기 때문에 Member와 Delivery의 주소를 order 마다 select 해줘야함

  • order에서 조회 1번
  • order.getMember().getName() 조회시 지연로딩으로 N번
  • order.getDelivery().getAddress() 조회시 지연로딩으로 N번

근데 이때 지연로딩은 영속성 컨텍스트에서 조회하는 것 이므로 중복으로 조회했던 경우엔 쿼리를 생략한다.

따라서 이때는 fetch join을 사용하는것이 좋다.

N+N 번 쿼리 예시


3. 엔티티를 DTO로 변환 - fetch join 최적화

/**
* V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용 O)
*/
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
      List<Order> orders = orderRepository.findAllWithMemberDelivery();
      return orders.stream().map(SimpleOrderDto::new).collect(Collectors.toList());
}

OrderRepository.java

// 한번의 쿼리로 order, member, delivery 를 다 join 해서 한번에 select 해온다.
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();
}

v2 예제 이미지처럼 쿼리가 N번 즉, 여러번 나가는 문제를 fetch join을 사용해서 한방쿼리로 성능을 최적화 할 수 있다.

또한 이렇게 fetch join을 사용하면 이미 order의 member와 order의 delivery 모두 조회 된 상태이므로 지연로딩이 필요없음

 

fetch join 사용 시 쿼리 예시

 

→ fetch join은 잘 활용 할수록 좋다!!


4. JPA에서 DTO로 바로 조회

new 명령어를 사용해 JPQL의 결과를 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();
    }

위와같은 사용은 리포지토리 재사용성이 떨어진다.

→ API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

 

쿼리 방식 선택 권장 순서

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

'JAVA' 카테고리의 다른 글

spring boot 프로젝트 생성과 사용 이유  (0) 2022.07.04
[JAVA] API 구현 시 테스트 개발  (0) 2022.06.02
[JAVA] CRU API  (0) 2022.05.17
[JAVA] boolean @Getter  (0) 2022.03.24
[TEST] Memory DB 사용  (0) 2022.03.16