[Spring Error] 무한 순환 참조 에러

thdtjdals__·2023년 12월 1일

Spring Error

목록 보기
1/3
post-thumbnail
WARN 25144 --- [nio-8080-exec-1] .c.j.MappingJackson2HttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class com.example.SignServer.Dto.CommentDto]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed/back reference 'defaultReference': no back reference property found from type java.util.Set<com.example.SignServer.Entity.Comment>
WARN 25144 --- [nio-8080-exec-1] .c.j.MappingJackson2HttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class com.example.SignServer.Dto.CommentDto]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed/back reference 'defaultReference': no back reference property found from type java.util.Set<com.example.SignServer.Entity.Comment>
WARN 25144 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=UTF-8' not supported]
2023-11-19 18:27:56.346  WARN 9180 --- [nio-8080-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Already had POJO for id (java.lang.Long) [[ObjectId: key=1, type=com.fasterxml.jackson.databind.deser.impl.PropertyBasedObjectIdGenerator, scope=java.lang.Object]]; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Already had POJO for id (java.lang.Long) [[ObjectId: key=1, type=com.fasterxml.jackson.databind.deser.impl.PropertyBasedObjectIdGenerator, scope=java.lang.Object]] (through reference chain: com.example.SignServer.Dto.CommentDto["pollId"]->com.example.SignServer.Entity.Poll["id"])]

스프링으로 커뮤니티 게시판에서 댓글에 대한 답글, 대댓글 기능과 좋아요 기능을 구현 중에 이 두 오류가 계속해서 반복적으로 발생하였습니다.

아무리봐도 코드엔 별문제가 없어보이는데,,

오류에 대해서 찾아보니 Jackson이 JSON을 엔티티 객체로 역직렬화할 때 순환 참조(circular reference) 혹은 양방향 참조(bidirectional reference)를 처리하지 못하여 발생하는 문제와 주로 JSON 직렬화 과정에서 발생하는 오류로 같은 ID를 가진 객체가 두 번 이상 참조되어 발생하는 것이라고 합니다.

즉 CommentDto에서 pollId 필드가 Poll 객체를 참조하고 있어서, 동일한 Poll 객체를 두 번 이상 참조하게 되면 발생하는 것이라 합니다.

커뮤니티 앱 구축에 제가 생성한 테이블은 각각 UserEntity, Poll, Comment 세 가지 엔티티 클래스 입니다. UserEntity는 사용자 정보를, Poll은 투표 정보를, Comment는 댓글 정보를 담고 있는데, 이 세 가지 엔티티 클래스들은 서로 복잡하게 상호 참조하고 있어 순환 참조 오류가 발생한 것 같습니다.

즉 이 오류는 객체 간의 상호 참조가 복잡해질 때 발생하는데 이는 특히 JSON 직렬화 과정에서 문제를 일으켜 오류가 발생한 것으로 보입니다.

예를 들어 UserEntity는 좋아한 댓글(likedComments)를 가지고 있고, Comment는 좋아하는 사용자(likedUsers)를 가지고 있습니다. 이런 상호 참조는 JSON 직렬화 과정에서 순환 참조 오류를 발생시킵니다.

public class UserEntity {
    @OneToMany(mappedBy = "user")
    private Set<Comment> comments;
}

public class Comment {
    @ManyToOne(fetch = FetchType.LAZY)
    private UserEntity user;
}

이는 댓글에 대한 좋아요를 한 유저가 중복 방지하기 위해서 위와같은 로직을 구현했으나 UserEntity 클래스와 Comment 클래스가 양방향 참조를 통해 무한히 순환하는 결과를 내서 오류가 발생한 것이라 생각이 듭니다.

public class UserEntity {
    @OneToMany(mappedBy = "user")
    @JsonManagedReference
    private Set<Comment> comments;
}

public class Comment {
    @ManyToOne(fetch = FetchType.LAZY)
    @JsonBackReference
    private UserEntity user;
}

오류를 해결하기 위해 @JsonManagedReference 어노테이션과 @JsonBackReference 어노테이션을 추가했습니다.

@JsonManagedReference는 순환 참조의 "부모" 쪽에 위치 시키고 @JsonBackReference는 "자식" 쪽에 위치시켜주면 됩니다.

이렇게 설정하면 "부모"는 자신의 "자식"을 직렬화하지만 "자식"은 자신의 "부모"를 직렬화하지 않게되며 무한한 순환 참조를 방지할 수 있을 것입니다.

하지만 이 방법은 부모자식간의 관계가 확실히 주어져야하며 또다시 똑같은 오류를 발생시켰습니다,,

@JsonIdentityInfo(
  generator = ObjectIdGenerators.PropertyGenerator.class, 
  property = "id")
public class UserEntity {
    ...
}

동일한 오류 해결을 위해 엔티티 클래스에서 @JsonIdentityInfo 어노테이션을 추가해줬습니다.

이 방법은 순환 참조를 해결하는 한 가지 방법일 뿐이며 경우에 따라서는 @JsonIdentityInfo 어노테이션을 사용하여 문제를 해결할 수도 있습니다. @JsonIdentityInfo는 엔티티 인스턴스마다 고유한 ID를 부여하여 같은 인스턴스를 참조하는 경우에는 그 ID를 사용하게 합니다. 이는 순환 참조로 인한 무한 루프를 방지할 수 있습니다.

하지만 이 방법까지 해봤으나 동일한 오류가 발생합니다,,

@ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "user_liked_comments",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "comment_id"))
    @JsonIgnore
    private Set<Comment> likedComments = new HashSet<>();
    
@ManyToMany(mappedBy = "likedPolls")
    @JsonIgnore
    private Set<UserEntity> likedUsers = new HashSet<>();
    
@ManyToMany(mappedBy = "likedComments")
    @JsonIgnore
    private Set<UserEntity> likedUsers = new HashSet<>();

다시 발생한 무한 순환 참조 오류 해결을 위해 @JsonIgnore 어노테이션을 사용해주었습니다.

@JsonIgnore는 JSON 직렬화 과정에서 특정 필드를 무시하도록 지정하는데 사용됩니다. 이를 통해 순환 참조로 인한 문제를 방지할 수 있습니다.

UserEntity, Poll, Comment 클래스의 likedUsers와 likedComments 필드에 @JsonIgnore를 추가하였습니다. 이렇게 하면 해당 필드를 JSON 직렬화 과정에서 무시하므로 순환 참조 오류를 방지할 수 있었습니다.

결론적으로 순환 참조 문제를 해결하기 위해 @JsonManagedReference와 @JsonBackReference, @JsonIgnore을 적절하게 적용하면 된다는 것을 알 수 있었습니다.

근본적으로 처음부터 객체 설계를 할때 객체 간에 무한히 순환 참조가 되지 않게 설계하는 것이 가장 중요하다 생각합니다.

사진 출처 : 구글 이미지

profile
내가 보려고 만든 공부, 개발 정리

0개의 댓글