JPA 양방향 무한 루프 java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
JPA 양방향 무한 루프 java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
API에서 양방향관계 엔티티를 반환하는 예제를 작성하던 도중 발생하였다.
오류#
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:472) ~[tomcat-embed-core-9.0.60.jar:9.0.60]
at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.sendServerError(DefaultHandlerExceptionResolver.java:552) ~[spring-webmvc-5.3.18.jar:5.3.18]
at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.handleHttpMessageNotWritable(DefaultHandlerExceptionResolver.java:442) ~[spring-webmvc-5.3.18.jar:5.3.18]
at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.doResolveException(DefaultHandlerExceptionResolver.java:209) ~[spring-webmvc-5.3.18.jar:5.3.18]
at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:142) ~[spring-webmvc-5.3.18.jar:5.3.18]
at org.springframework.web.servlet.handler.HandlerExceptionResolverComposite.resolveException(HandlerExceptionResolverComposite.java:80) ~[spring-webmvc-5.3.18.jar:5.3.18]
...
원인#
- Member 엔티티와 Order는 1:N의 관계이며 양방향 관계로 되어있다.
- API에서 Members를 호출할때 Orders를 가지고 있는 데이터가 문제가 되었다.
- Member에서는 Orders 그리고 Order에서는 Member가 연결되어있다.
- Members를 Json 형태로 결과를 만들면서 무한 루프에 빠진다.
문제가된 소스#
Member.java
package jpabook.jpashop.domain;
import com.fasterxml.jackson.annotation.*;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
public class Member {
public Member() {
}
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
//@JsonManagedReference
//@JsonBackReference
//@JsonIgnore
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
Order.java
package jpabook.jpashop.domain;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import jpabook.jpashop.domain.item.Item;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "orders")
public class Order {
//protected Order() {}
@Id @GeneratedValue
@Column(name="order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@JoinColumn(name = "member_id") // Order의 member가 수정되면 Order의 외래키 값이 변경됩니다.
private Member member;
// mappedBy 연관관계의 주인인 OrderItem의 order로 매핑 되어있다는 뜻
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
// ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate; //주문시간
@Enumerated(EnumType.STRING) // EnumType.ORDINAL(숫자라 순서바뀌면 큰일)이 기본이지만 무조건 EnumType.STRING(문자 코드)
private OrderStatus status; // 주문상태 [ORDER, CANCEL]
//==연관관계 메서드 (양방향 연관관계시 추가)==//
public void setMember(Member member){
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem){
this.orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery){
this.delivery = delivery;
delivery.setOrder(this);
}
//== 생성 메서드==//
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){ // OrderItem... 여러개를 넘길 수 있음
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
for (OrderItem orderItem : orderItems){
System.out.println(
orderItem
);
order.addOrderItem(orderItem);
}
order.setStatus(OrderStatus.ORDER);
order.setOrderDate(LocalDateTime.now());
return order;
}
//==비즈니스 로직==//
/**
* 주문 취소
*/
public void cancel(){
// 배송이 완료된 주문은 취소가 불가
if (delivery.getStatus() == DeliveryStatus.COMP){
throw new IllegalStateException("이미 배송이 완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : this.orderItems){
orderItem.cancel();
}
}
//==조회 로직==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice(){
/*
int totalPrice = 0;
for (OrderItem orderItem : this.orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
*/
return this.orderItems.stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
}
}
해결책#
1. DTO로 바꾸어 사용할 데이터만 반환하여 사용#
엔티티를 사용하지 않고, DTO로 Member 값만 조회하여 반환
2. @JsonIdentityInfo 어노테이션을 추가해서 직렬화시 중복 생성 막음#
- Jackson 2.0 이후부터 새롭게 추가된 어노테이션입니다.
- 생성자는 ObjectIdGenerators.IntSequenceGenerator.class를 어노테이션에 상속을 받아서 사용합니다.
- IntSequenceGenerator => id 값이 number
- StringIdGenerator=> id 값이 UUID 형태일때 사용합니다.
사용방법#
엔티티를 대상으로 @JsonIdentityInfo 어노테이션을 적용합니다.
Member와 Order에 아래 어노테이션을 추가한다.
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "id")
Member.java
@Entity
@Getter @Setter
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "id")
public class Member {
Order.java
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "id")
@Table(name = "orders")
public class Order {
3. @JsonIgnore#
엔티티의 컬럼을 대상으로 적용하며 Json 직렬화에 제외할 컬럼을 지정합니다.
Member.java
...
@JsonIgnore
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
...
참조#
Tomcat exception Cannot call sendError() after the response has been committed? Spring Boot : Error :Cannot call sendError() after the response has been committed