인프런 Spring 강의 정리/실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

[스프링 부트와 JPA 활용 2] 3. API 개발 고급 - 지연 로딩과 조회 성능 최적화

예롱메롱 2024. 1. 24. 01:04
728x90
반응형

📌 간단한 주문 조회

✔ 주문 + 배송 정보 + 회원을 조회하는 API 생성
➡ 지연 로딩 떄문에 발생하는 성능 문제를 단계적으로 해결

 

 

📌 1. 엔티티를 직접 노출

🔶 OrderSimpleApiController

package jpabook.jpashop.api;

import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;
import jpabook.jpashop.repository.OrderRepository;
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
 */
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    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().getUsername(); //Lazy 강제 초기화
            order.getDelivery().getAddress(); //Lazy 강제 초기화
        }
        return all;
    }
}
  • 엔티티를 직접 노출하는 것은 좋지 않음
  • order -> memberorder -> delivery지연로딩
    • 실제 엔티티 대신에 프록시 존재
  • jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모름 ➡ 예외 발생
  • Hibernate5Module스프링 빈으로 등록하면 해결

 

 

⚡ 하이버네이트 모듈 등록

  • 스프링 부트 버전에 따라 모듈 방법이 다름
  • 스프링 부트 3.0부터는 javax -> jakarta로 변경되어서 지원 모듈도 다른 모듈을 등록해야 함

✔ 스프링 부트 3.0 미만 : Hibernate5Module 등록

  • build.gradle에 다음 라이브러리를 추가
  • implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
  • JpashopApplication에 다음 코드 추가
@Bean
Hibernate5Module hibernate5Module(){
    return new Hibernate5Module();
}

 

기본적으로 초기화 된 프록시 객체만 노출, 초기화 되지 않은 프록시 객체는 노출 안함

➡ 스프링 부트 3.0 이상을 사용하면 해당 에러 발생

  • java.lang.ClassNotFoundException: javax.persistence.Transient

✔ 스프링 부트 3.0 이상 : Hibernate5JakartaModule 등록

  • build.gradle에 다음 라이브러리를 추가
  • implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5- jakarta'
  • JpashopApplication에 다음 코드 추가 
@Bean
Hibernate5JakartaModule hibernate5Module(){
  return new Hibernate5JakartaModule();
}

 

➡ 기본적으로 초기화 된 프록시 객체만 노출, 노출되지 않은 프록시 객체는 노출 안함

➡ 지연로딩은 로딩 안됨

 

 

 

 

⚡ 다음과 같이 설정하면 강제로 지연 로딩 가능

@Bean
Hibernate5Module hibernate5Module(){

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

 }

이 옵션을 키면 order -> member, member -> orders 양방향 연관관계를 계속 로딩하게 됨

@JsonIgnore 옵션을 한 곳에 주어야 함

for (Order order : all) {
    order.getMember().getUsername(); //Lazy 강제 초기화
    order.getDelivery().getAddress(); //Lazy 강제 초기화
}

위에 옵션을 끄고 이렇게 필요한 것만 강제 초기화 가능

 

주의 

✔ 스프링 부트 3.0 이상이면 Hibernate5Module 대신에 Hibernate5JakartaModule 을 사용해야 함
✔ 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한곳을 @JsonIgnore 처리 해야 함.
➡ 안그 러면 양쪽을 서로 호출하면서 무한 루프가 걸림지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안됨
즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있음
✔ 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 짐항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용해라!(V3에서 설 명)

 

cf )
✔ 정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않음.
➡ Hibernate5Module 를 사용하기 보다는 DTO로 변환해서 반환하는 것이 더 좋은 방법

 

 

 

📌 2. 엔티티를 DTO로 변환

🔶 OrderSimpleApiController에 코드 추가

   /**
     * V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용X)
     * - 단점 : 지연로딩으로 쿼리 N번 호출
     */
    @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(Collectors.toList());
        return result;
    }

    @Data
    static class SimpleOrderDto{

        private Long orderId;
        private String username;
        private LocalDateTime  orderDate;
        private OrderStatus orderStatus;
        private Address address;

        public SimpleOrderDto(Order order){
            orderId = order.getId();
            username = order.getMember().getUsername();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
        }
    }
  • 엔티티를 DTO로 변환하는 일반적인 방법
  • 쿼리가 총 1 + N + N번 실행(v1과 쿼리수 결과는 같음)
    • order 조회 1번 (order 조회 결과 수가 N이 됨)
    • order -> member 지연 로딩 조회 N번
    • order -> delivery 지연 로딩 N번
    • ex) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행됨(최악의 경우)
      • 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략함

 

 

📌 3. 엔티티를 DTO로 변환 - 페치 조인 최적화

🔶 OrderSimpleApiController에 코드 추가

    /**
     * V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용O)
     * - fetch join으로 쿼리 1번 호출
     * 참고 : fetch join에 대한 자세한 내용은 JPA 기본편 참고 (매우매우 중요)
     */

    @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(Collectors.toList());

        return result;
    }

 

 

🔶 OrderRepository에 코드 추가

    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

 

 

 

📌 4. JPA에서 DTO로 바로 조회

🔶 OrderSimpleApiController에 코드 추가

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

 

 

🔶 OrderSimpleQueryRepository에 코드 추가

package jpabook.jpashop.repository.order.simplequery;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {

    private final EntityManager em;

    public List<OrderSimpleQueryDto> findOrderDtos(){
        return em.createQuery(
                "select new jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto()"+
                        " from Order o" +
                        " join o.member m"+
                        " join o.delivery d", OrderSimpleQueryDto.class)
                        .getResultList();
    }
}

 

 

🔶 OrderSimpleQueryDto에 코드 추가

package jpabook.jpashop.repository.order.simplequery;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Data;

import java.time.LocalDateTime;

@Data
public class OrderSimpleQueryDto {
    private Long orderId;
    private String username;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    public OrderSimpleQueryDto(Long orderId, String username, LocalDateTime orderDate, OrderStatus orderStatus, Address address){
        this.orderId = orderId;
        this.username = username;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}
  • 일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회
  • new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
  • SELECT절에서 원하는 데이터를 직접 선택하므로 DB 
    • 애플리케이션 네트워크 용량 최적화(생각보다 미비)
  • 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

 

 

 

⚡ 정리

  • 엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두 가지 방법은 각 장단점이 있음
    • 둘 중 상황에 따라서 더 나은 방법을 선택하면 됨
    • 엔티티로 조회하면 리포지토리 재사용성도 좋고 개발 단순해짐

 

⚡ 쿼리 방식 선택 권장 순서

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

 

 

<실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화_김영한>을 수강하고 작성한 글입니다

 

 

 

 

 


PREV

 

[스프링 부트와 JPA 활용 2] 2. API 개발 고급 - 조회용 샘플 데이터 입력

📌 조회용 샘플 데이터 입력 package jpabook.jpashop; import jakarta.annotation.PostConstruct; import jakarta.persistence.EntityManager; import jpabook.jpashop.domain.*; import jpabook.jpashop.domain.item.Book; import lombok.RequiredArgsConstruct

nyeroni.tistory.com

NEXT

 

[스프링 부트와 JPA 활용 2] 4. API 개발 고급 - 컬렉션 조회 최적화

🖇 주문 조회 주문내역에서 추가로 주문한 상품 정보를 추가로 조회 Order 기준으로 컬렉션인 OrderItem와 Item이 필요 ➡ 앞의 예제는 toOne(OneToOne, ManyToOne) 관계 이야기 ➡ 이번 예제는 컬렉션인 일

nyeroni.tistory.com

 

728x90
반응형