주문 API를 만들며 만난 jpa 지연로딩 문제와 주의점.
XToOne 성능 최적화에 대한 이야기이다.
우선 주문 API는 다음과 같은 구조다. (전부 XToOne 관계)
order - member (ManyToOne)
order - delivery (OneToOne)
order - orderitem : XToMany
@GetMapping("/orders")
public List<Order> orders() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName();
order.getDelivery().getStatus();
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getProduct().getTitle());
}
return all;
}
주의할 점 1
# 엔티티 노출 X
엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 것 꼭 한 쪽을 @JsonIgnore 처리해야한다.
아니면 서로 호출해서 무한루프가 걸림. 물론 DTO로 변환해야 하지만..
그래서 @JsonIgnore 처리를 하면?
"trace": "org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type,
class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ArrayList[0]->com.minji.vanillashop.domain.order.entity.Order["orderItems"]-
여전히 에러가 난다. 무슨 에러일까?
Order entity를 보면 다음과 같이 되어 있는데..
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
여기서 지연로딩을 살펴보자.
지연 로딩은 필요한 값을 디비에서 끌고 오는게 아니다. 프록시 기술로 뭔가 아래처럼
private Member member = new ByteBuddyInterceptor();
가짜로 넣어두고, 필요한 값을 꺼내려고 맴버 객체에 손을 대면 그 때 sql을 날려 값을 채워주는 식으로 동작한다. (이걸 프록시를 초기화 한다고 한다.) 이 과정에서 json이 member 객체를 꺼내가려고 하는 순간! 이게 순수 객체가 아니라 바이트 버디 인 것이다... 그래서 이런 에러가 발생한다. 이를 해결해주는 라이브러리로 해결할 수도 있지만, 사실 Entity를 노출하지 않으면 일어나지도 않을 문제다.
주의할 점 2
# api 스펙에 불필요한 정보 노출
api 스펙은 필요한 것만. 간단하게! 하자.
사용하지 않는 연관 정보까지 가지고 오니까 성능 저하도 생기고,
이렇게 안하면 실무 운영이 헬이 된다..
주의할 점 3
# 그렇다고 해서 즉시로딩(Eager)을 사용하지 말자.
Eager 로 해서 성능 최적화가 가능할때는 em.find로 식별자 하나로 할 때 정도다.
findAllByString 같이 JPQL이 날아갈땐 관련된 애들 쿼리를 다 날리기 때문에 성능 튜닝이 매우 힘들다.
지연로딩이 기본이고, 성능 최적화를 위해서 페치 조인을 사용하자.
코드를 아래처럼 수정했다.
@GetMapping("/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private OrderStatus orderStatus;
private List<OrderItemDto> orderItems;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(toList());
}
}
그런데 지연로딩을 사용하다보면 로딩은 쿼리가 너무 많이 나가는 것을 볼 수 있다.
주의할 점 4
# LAZY 로딩 n+1 문제
사실 1+N이다. 만약 위에서 주문이 10개면, 최악의 경우 21개의 쿼리가 나간다. ( 1 + member N + delivery N) 쿼리가 많이 나가면 네트워크 작업이 많으니 성능 저하 문제가 생기는데, 이 문제를 아래처럼 fetch join으로 해결할 수 있다.
# Fetch join
[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();
}
[OrderController]
@GetMapping("/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
그래서 정리하자면..
# 쿼리 방식 선택 권장 순서
1. 엔티티를 DTO로 변환 (유지 보수하기 좋음)
2. fetch join으로 성능 최적화. (여기까지 하면 보통 )
3. 2까지 했는데 안되면 DTO 직접 조회하는 방식 (포스팅에 정리안함)
사실 성능은 select절이 아니라 join, where 쪽에서 많이 일어나는데, 그래서 2와 3은 특수한 상황이 아니라면 큰 성능 차이가 없다. 특수한 상황이란 고객 트래픽이 실시간으로 마구 쏟아지는 api가 (예를들어 admin 페이지?)
4. 네이티브 SQL이나.. SQL을 직접 사용 (포스팅에 정리안함)
'🔗 JPA' 카테고리의 다른 글
OSIV(Open Session in view)이란? 장단점, 써야할지 말아야할지 (0) | 2022.10.21 |
---|---|
[JPA] 컬렉션 조회(1대다 관계) 최적화 (0) | 2022.10.14 |
[JPA] jdbcsqlintegrityconstraintviolationexception null not allowed for column (0) | 2022.10.14 |
Setter없이 Entity update (0) | 2022.10.09 |
[JPA] No Property 메소드명 found 에러 해결방법 (No property 'xx' found for type 'x') (0) | 2022.10.01 |
댓글