TIL(Today I Learned)

TIL - 댓글을 가져올 때 순환 참조 발생

Happy._. 2024. 5. 13. 22:43

Todo 프로젝트에 댓글 기능을 추가하면서 순환 참조 문제가 발생했다.

테스트 코드에서는 문제가 없었지만 swagger에서 테스트를 하려고 보니 Response Example Value에 문제가 있는 게 보였다.

comments 내 todo에 comments가 다시 들어가 있어서 순환 참조가 발생할 코드였다.

 

[Execute] 버튼을 누르면 예상대로 다음과 같이 순환참조 되면서 중복되는 많은 데이터가 출력된다.

 

서버 로그에는 다음과 같은 예외 메시지가 출력된다.

2024-05-13T20:25:33.474+09:00  WARN 12560 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Ignoring exception, response committed already: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError)
2024-05-13T20:25:33.475+09:00  WARN 12560 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError)]

 

이 문제를 어떻게 해결해야 될까 찾아보다가 Json 라이브러리의 어노테이션을 사용하는 방법이 있어서 시도해 보았다.

 

Json 라이브러리의 @JsonManagedReference와 @JsonBackReference

@JsonManagedReference : 부모 클래스 (Todo Entity)에 추가 - 1 쪽
@JsonBackReference : 자식 클래스 (Comment Entity)에 추가 - N 쪽

@Entity
@Table(name = "todo")
class Todo(
    // ....

    @OneToMany(mappedBy = "todo", fetch = FetchType.EAGER, cascade = [CascadeType.ALL], orphanRemoval = true)
    @JsonManagedReference
    var comments: MutableList<Comment> = mutableListOf(),
) {
    // ...
}
@Entity
@Table(name = "comment")
class Comment(
    // ...

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "todo_id")
    @JsonBackReference
    val todo: Todo
) {
    // ...
}

 

결과는 다음과 같다.

데이터는 정상적으로 출력되지만 출력되지 말아야 할 password까지 출력되었다.

 

그리고 Response Example Value는 여전히 순환 참조가 발생 코드로 남아있다.

 

이번에는 위에서 추가했던 어노테이션들을 제거하고 Entity 객체를 그대로 반환하던 방식에서 DTO를 사용했다.

data class TodoResponse(
    // ...
    val comments: List<CommentResponse> // List<Comment> -> List<CommentResponse>
)
@Entity
@Table(name = "todo")
class Todo(
    // ...
) {
    // ...
}

fun Todo.toResponse(): TodoResponse {
    return TodoResponse(id!!, title, content, writer, createAt, completed, comments.map { it.toResponse() })
}

 

위와 같이 map 함수를 사용해 DTO로 변환시켜서 응답을 보내면 순환 참조 없이 정상적으로 데이터가 출력되었다.

 

Example Value에도 다음과 같이 필요한 데이터만 출력이 되었다.

 

결론: DTO를 잘 활용하자.