프로그래밍/spring boot

[스프링부트] 실전! 스프링 부트와 JPA 활용2 지연 로딩과 조회 성능 최적화 #1 간단한 주문 조회 V1: 엔티티를 직접 노출

aSpring 2023. 11. 15. 12:44
728x90
728x90
※ 본 포스팅은 김영한 강사님의 인프런 '실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화' 강의를 들으며 작성한 수강일지 입니다.

 
 

 

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

1. 간단한 주문 조회 V1: 엔티티를 직접 노출
2. 간단한 주문 조회 V2: 엔티티를 DTO로 변환
3. 간단한 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화
4. 간단한 주문 조회 V4: JPA에서 DTO로 바로 조회

 

주문을 기반으로, 주문 + 배송정보 + 회원을 조회하는 API를 만들자

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

 

참고: 지금부터 설명하는 내용은 정말 중요합니다. 실무에서 JPA를 사용하려면 100% 이해해야 합니다.
안그러면 엄청난 시간을 날리고 강사를 원망하면서 인생을 허비하게 됩니다.

 

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

OrderSimpleApiController

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 관계
 * Order
 * Order -> Member : ManyToOne 관계
 * Order -> Delivery : OneToOne 관계
 *
 * xToMany : Collection
* */
@RestController
@RequiredArgsConstructor
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;
    }
}

 

xToOne 관계를 어떻게 성능 최적화 할것인가?

  • 엔티티를 직접 노출하는 것은 좋지 않다. (앞장에서 이미 설명)
  • order member 와 order address 는 지연 로딩이다. 따라서 실제 엔티티 대신에 프록시 존재
  • jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모름 -> 예외 발생
  • Hibernate5Module 을 스프링 빈으로 등록하면 해결(스프링 부트 사용중)

member, orderItems, delivery는 가져오지 않아 null로 보임

 

하이버네이트 모듈 등록

스프링 부트 버전에 따라서 모듈 등록 방법이 다르다.

스프링 부트 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 옵션을 한곳에 주어야 한다.

member 등 연관된 것들 강제로 lazy loaing해서 다 가지고 옴

-> 이렇게 해서 Entity를 그대로 다 노출하면 안됨!!! -> Entity 바뀌면 api 스펙이 다 바뀌어버리기 때문에

 

force lazy loading을 끄고, 내가 원하는 것들만 출력하기(member, delivery)

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

    Hibernate5Module hibernate5Module = new Hibernate5Module();
//		hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true); // 강제로 Lazy loading -> 이런 옵션은 사실 사용하면 안됨
    return hibernate5Module;
}
package jpabook.jpashop.api;

import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
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 관계
 * Order
 * Order -> Member : ManyToOne 관계
 * Order -> Delivery : OneToOne 관계
 *
 * xToMany : Collection
* */
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

    // 계속 참조가 일어나면서 loop에 빠짐
    // 양방향 연관관계가 있다 ? 무조건 한쪽은 JsonIgnore 해주어야 함
    @GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        // force lazy loading 끄고 내가 원하는 값만 들고오기
        for (Order order : all) { // 이렇게 원하는 값만 강제 lazy loading
            order.getMember() // 여기까지는 아직 proxy 객체 -> 쿼리가 아직 안 날라감
                    .getName(); // Member에 쿼리 날려서 Lazy 강제 초기화
            order.getDelivery().getAddress();
        }
        return all;
    }
}

- 이것도 member에서 원하는 name만 가져온다거나.. 필요한 값만 가지고 와야 함

-> 이 데이터를 안쓰면 상관없으나, 사용한다면 문제가 될 수 있음

 

주의: 스프링 부트 3.0 이상이면 Hibernate5Module 대신에 Hibernate5JakartaModule 을 사용해야 한다.

 

주의: 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한곳을 @JsonIgnore 처리 해야 한다. 안그러면 양쪽을 서로 호출하면서 무한 루프가 걸린다.

 

참고: 앞에서 계속 강조했듯이 정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않다. 따라서 Hibernate5Module 를 사용하기 보다는 DTO로 변환해서 반환하는 것이 더 좋은 방법이다.

 

주의: 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다! 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 진다.
항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용해라!(V3에 서 설명)
728x90
728x90