[JPA 활용2] API 개발 고급 - 지연 로딩과 조회 성능 최적화 ①

kiteB·2021년 11월 23일
0

JPA

목록 보기
24/28
post-thumbnail

오랜만에 Spring JPA 공부를 해보자 ㅎㅎ

지난 내용은 여기 참고

이번 시간에는 주문 + 배송정보 + 회원을 조회하는 API 만들어볼 것이다.
지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해보자.

실무에서 JPA를 사용하려면 꼭 필요한 내용이므로 집중 ❗❗


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

1. OrderSimpleApiController에 코드 추가

@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {

    private final OrderRepository orderRepository;

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

위와 같이 코드를 작성한 뒤, http://localhost:8080/api/v1/simple-orders로 접속하면
다음과 같이 엔티티가 무한으로 호출되는 결과를 확인할 수 있다.

이는 ordermember가 서로를 참조하기 때문에 무한순회를 하기 때문이다.

2. 양방향 연관관계 문제 해결

위와 같이 무한순회를 방지하기 위해서는 @JsonIgnore를 추가해주어야 한다.

  • Member.java
@JsonIgnore
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
  • OrderItem.java
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order; //주문
  • Delivery.java
@JsonIgnore
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();

위와 같이 @JsonIgnore를 추가해주자!

다시 http://localhost:8080/api/v1/simple-orders로 접속하면 다음과 같이 출력되는 다른 문제가 발생한다!

ByteBuddyInterceptor에서 Type definition error가 발생하였다.

ordermember, address지연 로딩이므로 실제 엔티티가 아닌 프록시 객체가 존재한다. jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 생성하는 방법을 모른다 🤷🏻‍♀️ 그래서 이와 같은 예외가 발생하는 것이다.

3. Hibernate5Module 등록

위와 같은 문제를 해결하기 위해 Hibernate5Module을 스프링 Bean으로 등록해주면 된다!

먼저 build.gradle에 다음과 같은 라이브러리를 추가해준 뒤,

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

JpashopApplication에 다음과 같은 코드를 추가해주자.

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

그러면 이제 다음과 같이 잘 출력된다.

📌 정리

1. 양방향 연관관계인 경우, @JsonIgnore 처리해주기

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

2. 엔티티를 직접 노출하는 것은 좋지 않다!

  • Hibernate5Module를 사용하기 보다는 DTO로 변환해서 반환하는 것이 좋은 방법이다!
  • 엔티티를 그대로 노출하면 나중에 엔티티가 바뀌면 API 스펙이 다 바뀐다.

3. 지연 로딩을 피하기 위해 즉시 로딩으로 설정하면 안 된다!

  • 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다.
  • 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용해라! → V3

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

OrderSimpleApiController - V2 추가

@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> orderV2() {
    return orderRepository.findAllByString(new OrderSearch()).stream()
        .map(SimpleOrderDto::new)
        .collect(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();
        orderDate = order.getOrderDate();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress();
    }
}
  • 엔티티를 DTO로 변환하는 일반적인 방법이다.

실행 결과

📌 V1, V2의 문제점

  • 쿼리가 총 1 + N + N번 호출된다.
    • order 조회 1번
    • order → member 지연 로딩 조회 N번
    • order → delivery 지연 로딩 조회 N번

Order 조회가 많아질수록 쿼리 수행이 많아지고 성능 저하가 일어날 수 있다!

profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글