[Trouble Shooting] JPA 양방향 참조 문제

이민호·2025년 4월 12일

2025-04-12T15:11:23.683+09:00 WARN 12908 --- [nio-8080-exec-6] L.L.a.exception.ExceptionAdvice : Response already committed. Ignoring: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Document nesting depth (1001) exceeds the maximum allowed (1000, from StreamWriteConstraints.getMaxNestingDepth())

Service 로직을 작성하던 도중, 이와 같은 오류를 만나게 되었다.
오류를 관련해서 찾아보니 jackson 라이브러리가 객체를 직렬화할 때, 각 엔티티의 연관관계를 무한참조하면서 직렬화 깊이가 중첩되서 생기는 문제였다.

문제가 발생한 부분은

 List<File> Files = fileRepository.findAllByCommitId(commitId);

        // DTO로 변환
        CommitResponseDTO commitResponseDTO = new CommitResponseDTO(
                commit.getId(),
                commit.getUser().getId(),
                commit.getMessage(),
                commit.getStats(),
                commit.getDate()
        );

        return new CommitDetailResponseDTO(commitResponseDTO, Files);

중 return 문이었다.

프로젝트 설계상, Commit 엔티티와 File 엔티티는 1대다 연관관계로 구성되어 있고,
CommitDetailResponse는 한 Commit에 대한 정보 아래에 그 커밋의 변경 파일들을 리스트로 제공할 생각이었다.

그래서 CommitDetailResponseDTO를

public class CommitDetailResponseDTO {
    private CommitResponseDTO commitResponseDTO;
    private List<File> files;
}

이런식으로 구성하였고, 여기서 이 객체를 json하는 과정에서

Commit Entity의

@OneToMany(mappedBy = "commit", cascade = CascadeType.ALL)
    private List<File> files = new ArrayList<>();

부분과,

File Entitiy

@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "commit_id", nullable = false)
    private Commit commit;

부분이 무한으로 순환 참조되면서, 발생한 문제다.

🧶근본적 원인

순환 직렬화 문제의 핵심은 "다" 쪽에서 "일"(상위)을 계속 참조하는 것이다. (엔티티의 관계)

즉,
이 문제는 File 객체가 양방향 관계를 통해 Commit을 참조하면서 순환 직렬화가 발생한 것이 원인이었다.

이를 해결하기 위해, "다" 관계에 해당하는 File 객체에서 Commit 엔티티를 직렬화 대상에서 제외할 필요가 있었다.

🎨해결

해결법은 두 가지가 있지만, 난 방법2를 채택해서 해결하였다.

방법 1. @JsonIgnore, @JsonBackReference 사용

  • 직렬화 할 entity 쪽엔 @JsonManagedReference
  • 직렬화에서 제외할 entity 쪽엔 @JsonBackReference을 해결한다.
    이 두 어노테이션을 짝으로 사용하므로써, 부모(일)-자식(다) 관계에서 순환 참조를 방지할 수 있다.

성공 된 것을 확인.

방법 2. (선택) File 객체에 대한 DTO 만들기.

난 File 엔티티를 직접 전달함으로써 발생하는 문제라고 생각했다.

하지만, 우리는 Commit하나에 대한 file들을 참조하는것이 필요했지 file에서 Commit을 거슬러 참조할 필요는 없었기에, (CommitDetailResponseDTO(commitResponseDTO, Files) 부분에 이미 Commit 정보를 넣어주기때문에..)

단순히 FileResponseDTO를 만들어, Commit entity에 대한 정보를 제공하지 않으면 됐다.

그리고 단순히 annotation을 다는 것 보다는, DTO를 사용해 개발자가 직접 객체를 다루는 것이 유지보수나 유연성 측면에서 좋다고 생각했기 때문이다.


FileResponseDTO를 매핑.

성공한 것을 확인.

@JsonIgnore을 이용해 필드자체를 제외도 있긴 했지만, 사실상 직렬화 필드 자체를 막는것이기에 사용하지 않았다.

profile
효율적으로 살게요.

0개의 댓글