JSON 순환 참조 문제

Kim jisu·2025년 2월 21일
0

 Debugging Note

목록 보기
14/37

서비스를 개발하면서 마주친 JSON 순환 참조 문제와 그 해결 과정 정리.

문제 상황

게시글 상세 조회 API를 개발하던 중, 다음과 같은 에러 메시지를 만났습니다.

{
  "status": "fail",
  "data": {
    "code": "UNK000",
    "message": "알 수 없는 에러 발생",
    "details": "Could not write JSON: Infinite recursion (StackOverflowError)"
  }
}

처음에는 단순한 에러라고 생각했는데, 로그를 자세히 살펴보니 JSON 응답이 무한히 반복되는 현상을 발견했습니다.

원인 분석

문제의 원인을 찾기 위해 엔티티 구조를 살펴보았습니다.

@Entity
public class Post {
    @OneToMany(mappedBy = "post")
    private List<Comment> comments;
}

@Entity
public class Comment {
    @ManyToOne
    private Post post;
}

그러자 문제의 원인이 보였습니다. Post와 Comment 엔티티가 서로를 참조하는 양방향 관계를 가지고 있었고, 이로 인해 JSON 직렬화 과정에서 다음과 같은 무한 순환이 발생했던 것입니다:

Post -> comments -> Comment -> post -> comments -> Comment -> ...

해결 방안 탐색

이 문제를 해결하기 위한 몇 가지 방법을 검토해보았습니다.

  1. @JsonManagedReference와 @JsonBackReference 사용

    public class Post {
        @JsonManagedReference
        private List<Comment> comments;
    }
    
    public class Comment {
        @JsonBackReference
        private Post post;
    }

    간단하지만, 엔티티에 표현 계층의 책임이 포함되는 단점이 있었습니다.

  2. @JsonIgnore 사용

    public class Comment {
        @JsonIgnore
        private Post post;
    }

    마찬가지로 간단하지만, 유연성이 떨어지는 해결책이었습니다.

  3. DTO 패턴 사용
    엔티티와 표현 계층을 명확히 분리할 수 있는 방법이었습니다.

최종 해결책: DTO 패턴 적용

여러 방안을 검토한 끝에, DTO 패턴을 사용하기로 결정했습니다. 이유는 다음과 같습니다:

  1. 엔티티와 표현 계층의 명확한 분리
  2. API 스펙 변경에 유연하게 대응 가능
  3. 필요한 데이터만 선택적으로 전송 가능
@Getter
@Builder
public class PostDTO {
    private Long id;
    private String title;
    private String content;
    private List<CommentDTO> comments;

    public static PostDTO from(Post post) {
        return PostDTO.builder()
                .id(post.getId())
                .title(post.getTitle())
                .content(post.getContent())
                .comments(post.getComments().stream()
                        .map(CommentDTO::from)
                        .collect(Collectors.toList()))
                .build();
    }
}

서비스 계층도 다음과 같이 수정했습니다:

@Transactional
public PostDTO getPostById(Long id) {
    Post post = postRepository.findPostById(id);
    post.countViews();
    return PostDTO.from(post);
}

추가 고민거리: 조회수 처리

DTO 패턴을 적용하면서 조회수 증가 로직도 함께 고민했습니다. 트래픽이 많아질 경우를 대비해 다음과 같은 최적화 방안도 고려해보았습니다:

  1. Redis를 활용한 캐시 처리
  2. 비동기 처리를 통한 성능 개선
  3. 배치 처리를 통한 주기적 동기화

하지만 현재 서비스 규모에서는 단순한 구현으로도 충분하다고 판단하여, 기본적인 트랜잭션 처리만 유지하기로 했습니다.

마치며

이번 경험을 통해 몇 가지 교훈을 얻었습니다:

  1. 엔티티 설계 시 순환 참조 가능성을 항상 고려해야 합니다.
  2. 문제 해결 시 당장의 해결책보다 장기적인 관점에서 접근하는 것이 중요합니다.
  3. 때로는 가장 단순한 해결책이 최선의 선택일 수 있습니다.😊
profile
Dreamer

0개의 댓글