2022. 5. 25. 16:09ㆍJava/JPA
앞으로 4개의 포스팅에 걸쳐서 지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해보겠습니다.
오늘은 첫 번째 시간으로 V1(버전 1)에 대해서 정리하고 문제점과 주의점 그리고 대안에 대해서 정리해보겠습니다.
추천드리는 방법은 아니기 때문에 가볍게 보시면 될 것 같습니다.
결론 : 엔티티를 직접 노출하지 말자!
간단한 주문 조회 V1 : 엔티티를 직접 노출
주문(Order) + 배송정보(Delivery) + 회원(Member)을 조회하는 API를 만들어보겠습니다.
OrderSimpleApiController
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Getter;
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;
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {// 엔티티 그대로 반환(엔티티 그대로 노출)
List<Order> all = orderRepository.findAllByString(new OrderSearch());
return all;
}
}
이렇게 적고 구동 했을 시 무한루프가 걸린 것을 확인했습니다.
* 무한루프 : Order에서 Member 갔는데, Member에도 Order 있어서 결과적으로 양쪽을 서로 호출하면서
무한 루프가 걸린다. (양방향 연관관계 문제 생김)
해결 : 한쪽을 @JsonIgnore처리
Member
package jpabook.jpashop.domain;
@Entity
@Getter @Setter
public class Member {
@JsonIgnore
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
OrderItem
package jpabook.jpashop.domain;
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id") // order_id(FK)
@JsonIgnore
private Order order;
}
Delivery
package jpabook.jpashop.domain;
@Entity
@Getter
@Setter
public class Delivery {
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
@JsonIgnore
private Order order;
}
한쪽을 @JsonIgnore처리하고 구동했는데 이번에는
또 다른 문제가 발생했습니다.
음,,?
에러 코드를 보면 proxy,, bytebuddy... 이런 내용이 있습니다.
order → member와 order → address는 지연 로딩이므로 실제 엔티티가 아닌 프록시로 존재하는데요,
jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모르기 때문에 발생하는 예외입니다.
해결
Hibernate5 Module을 스프링 빈으로 등록
JpaShopApplication.java 추가
package jpabook.jpashop;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class JpashopApplication {
public static void main(String[] args) {
SpringApplication.run(JpashopApplication.class, args);
}
// 추가
@Bean
Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
}
build.gradle 에 다음 라이브러리를 추가
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
Hibernate5 Module을 스프링 빈으로 등록했더니 잘 나온 것을 확인할 수 있었습니다.
여기서 왜 null이라고 뜨는지 궁금하신 분들도 계실 텐데요.
이는 지연 로딩이기 때문에 DB에서 조회한 게 아니므로 null로 표시되는 것입니다.
물론 이 부분도 데이터를 보고 싶다면 강제로 지연 로딩하면 되겠지만, 사용하지 않는 것을 추천드립니다.
이처럼 엔티티를 그대로 노출 시
1. 엔티티를 수정하면 API스펙이 다 바뀌는 문제뿐만이니라
2. 사용하지 않는 엔티티 조회 쿼리까지 나감으로써 성능 문제도 야기합니다.
포스팅을 시작하면서 V1은 권장하는 방법이 아니라고 적었는데 막상 정리하다 보니 길어진 감이 있는데요;;
여러분은 이 '정리'내용만 보셔도 될 것 같습니다!
정리
주의: 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한 곳을 @JsonIgnore 처리해야 합니다.
안 그러면 양쪽을 서로 호출하면서 무한 루프가 걸립니다.
주의: 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안 됩니다.
즉시 로딩은 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있고 성능 튜닝이 매우 어려워집니다.
항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용하는 것을 추천드립니다. (V3에서 설명)
엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않습니다.
(앞서 사용한 Hibernate5 Module 보단) DTO로 변환해서 반환하는 것이 더 좋은 방법입니다.
결론 : 엔티티를 직접 노출하지 말자!
만약 프록시, 지연 로딩에 대해 궁금하시다면 가볍게 읽고 오시는 것을 추천합니다.
참고 :
'Java > JPA' 카테고리의 다른 글
[API 개발 고급] ❗지연 로딩과 조회 성능 최적화(V3) 3/4 (0) | 2022.05.25 |
---|---|
[API 개발 고급] 지연 로딩과 조회 성능 최적화(V2) 2/4 (0) | 2022.05.25 |
영속성 전이(CASCADE)와 고아 객체 (0) | 2022.05.25 |
즉시 로딩과 지연 로딩 (0) | 2022.05.25 |
프록시(Proxy) (0) | 2022.05.25 |