Programming/Spring Boot

양방향 매핑 순환참조 문제 Cannot call sendError() after the response has been committed

Jan92 2023. 4. 28. 22:02

양방향 매핑 순환참조 문제 해결 방법(@JsonIgnore, @JsonManagedReference, @JsonBackReference, @JsonIdentityInfo)

"Cannot call sendError() after the response has been committed"

 

//User Entity
@NoArgsConstructor
@Getter
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String name;

    @OneToMany(mappedBy = "user")
    private List<Account> accounts = new ArrayList<>();
}

//Account Entity
@NoArgsConstructor
@Getter
@Entity
public class Account {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
	
    @Column
    private String alias;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

(User Entity, Account Entity)

양방향 연관관계

다음과 같이 User, Account 두 객체가 ManyToOne, OneToMany '양방향 관계'에 있을 때, User Entity를 조회하는 과정에서 아래와 같은 순환참조 문제가 발생하였습니다.

 

 

양방향 매핑 순환참조 문제

Spring Boot에서는 Controller에 @ResponseBody 선언 시(= @RestController 선언 시) 응답 객체를 JSON 형태로 직렬화하기 위해 HttpMessageConverters에서 Jackson Library를 사용하는데요.

 

Entity 객체를 그대로 JSON 문자열로 변환하는 과정에서 User 객체의 accounts 필드가 Account Entity를 참조하고, Account 객체의 User 필드가 User Entity를 참조하며 다음과 같은 순환참조 문제가 발생하게 된 것입니다.

 

 


 

***

위와 같은 순환참조 문제는 com.fasterxml.jackson.annotation package에 있는 어노테이션을 통해 쉽게 해결할 수 있는데요.

 

아래 내용을 통해 순환참조를 해결할 수 있는 어노테이션인 @JsonIgnore, @JsonManagedReference, @JsonBackReference, @JsonIdentityInfo의 사용법과 내용 살펴보겠습니다.

 

 

1. @JsonIgnore

@NoArgsConstructor
@Getter
@Entity
public class Account {

    ...

    @JsonIgnore
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

(Account Entity의 user 필드에 @JsonIgnore 어노테이션 적용)

 

첫 번째는 @JsonIgnore 어노테이션을 이용한 방법입니다.

@JsonIgnore 어노테이션의 경우 해당 어노테이션이 달린 속성에 대해 직렬화 또는 역직렬화되지 않도록 무시하는 기능이 적용되는데요.

 

때문에 이러한 기능을 이용하여 Account Entity의 user 필드에 @JsonIgnore를 적용해 주면, User 객체의 accounts 필드가 Account Entity를 참조하고, 이후 Account 객체의 user 필드에 대해 직렬화가 되지 않도록 무시하기 때문에 더 이상 순환참조가 발생하지 않게 됩니다.

 

 

2. @JsonManagedReference, @JsonBackReference

//User Entity
@NoArgsConstructor
@Getter
@Entity
public class User {

    ...

    @JsonManagedReference
    @OneToMany(mappedBy = "user")
    private List<Account> accounts = new ArrayList<>();
}

//Account Entity
@NoArgsConstructor
@Getter
@Entity
public class Account {

    ...

    @JsonBackReference
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

(User Entity의 accounts 필드에 @JsonManagedReference, Account Entity의 user 필드에 @JsonBackReference)

 

두 번째 @JsonManagedReference@JsonBackReference 어노테이션을 이용한 방법입니다.

이 두 어노테이션은 애초에 양방향 관계에서의 순환참조를 방어하기 위해 설계되었는데요.

 

@JsonManagedReference는 양방향 연관 관계에서 주인이 아닌 Entity에 선언되며, 해당 어노테이션이 적용된 필드에 대해서는 직렬화가 정상적으로 수행됩니다.

@JsonBackReference는 양뱡향 연관 관계에서 주인 Entity에 선언되며, 해당 어노테이션이 적용된 필드에 대해서는 직렬화가 수행되지 않습니다.

 

 

3. @JsonIdentityInfo

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
@NoArgsConstructor
@Getter
@Entity
public class User {

    ...

    @OneToMany(mappedBy = "user")
    private List<Account> accounts = new ArrayList<>();
}

(User Entity 자체에 @JsonIdentityInfo 어노테이션 적용)

 

세 번째는 @JsonIdentityInfo 어노테이션을 이용한 방법입니다.

@JsonIdentityInfo는 객체의 identity 정보를 지정하는 데 사용되며, 이 정보는 JSON으로 변환될 때 객체를 구분하는 용도로 쓰이는데요.

generator 속성의 경우 객체의 identity 정보를 생성하는 데 사용할 generator 클래스를 지정하는 것이며, property의 경우 identity 정보를 저장할 속성 이름을 지정하는 것입니다.

(property의 default 값은 "@id"입니다.)

 

 

여기까지 양방향 매핑 관계에서 순환참조 문제가 발생했을 때 jackson annotation을 통해 해결하는 방법에 대해서 살펴봤는데요.

사실 순환참조가 발생하게 된 주요 원인 중 하나는 entity 자체를 response로 반환하는 것도 포함되기 때문에 꼭 entity를 반환해야 하는 경우가 아니라면 dto를 이용하여 순환참조 문제를 해결할 수도 있습니다.

 

또한 양방향 매핑이 꼭 필요한 게 아니라면 양방향 매핑을 제거하여 순환참조를 발생시키지 않는 것도 방법이 될 수 있습니다.

 

 

< 참고 자료 >

https://data-make.tistory.com/727