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]

...

원인#

  1. Member 엔티티와 Order는 1:N의 관계이며 양방향 관계로 되어있다.
  2. API에서 Members를 호출할때 Orders를 가지고 있는 데이터가 문제가 되었다.
  3. Member에서는 Orders 그리고 Order에서는 Member가 연결되어있다.
  4. 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

JPA 양방향 무한 루프 java.lang.IllegalStateException: Cannot call sendError() after the response has been committed