본문 바로가기
💻 트러블슈팅 (에러 해결)

포스트맨이 멈추지 않아요! JPA 순환 참조(무한 루프) 에러 원인과 DTO 해결법 (거울 방 비유)

by 데브프리 2026. 7. 21.

안녕하세요, 여러분의 든든한 IT 길잡이 데브프리입니다.

 

스프링 부트(Spring Boot)와 JPA를 이용해 게시판을 만들고, "게시글(Post)을 조회하면 그 안에 달린 댓글(Comment)들도 함께 보여줘야지!"라며 야심 차게 API를 완성했습니다. 그리고 포스트맨(Postman)에서 Send 버튼을 누르는 순간!

 

갑자기 포스트맨 화면의 JSON 데이터가 끝도 없이 주르륵 내려가더니, 서버 콘솔 창에는 아래와 같은 무시무시한 에러가 뿜어지며 서버가 뻗어버립니다.

 

java.lang.StackOverflowError

(또는 HttpMessageNotWritableException: Infinite recursion (StackOverflowError))

 

"데이터가 왜 끝없이 나오는 거지? 내가 해킹이라도 당한 건가?"

초보 백엔드 개발자들이 JPA 양방향 매핑을 배울 때 100% 확률로 마주치는 통과의례 같은 에러입니다.

 

오늘은 포스트맨을 멈추지 않게 만드는 공포의 'JPA 순환 참조(Infinite Recursion) 에러'의 원인을 아주 명쾌한 비유로 알아보고, 실무에서 사용하는 가장 우아한 해결법인 DTO까지 완벽하게 정리해 드리겠습니다!

 

1. Infinite recursion (순환 참조), 도대체 왜 일어나는 걸까? (거울 방 비유)

이 에러는 스프링 부트가 우리가 만든 자바 객체(Entity)를 프론트엔드가 읽을 수 있는 JSON 형태로 변환해 줄 때 발생하는 '무한 루프(Infinite Loop)' 현상입니다.

JPA 순환 참조 에러 무한 루프 거울 방 비유
순환 참조 에러는 엘리베이터의 마주 보는 두 거울처럼, 객체들이 서로를 끝없이 가리키며 무한 반사되는 현상입니다.

스프링 부트 안에는 Jackson이라는 이름의 아주 성실한 번역기(JSON 변환기) 로봇이 살고 있습니다. 이 로봇은 컨트롤러가 던져준 데이터 안을 샅샅이 뒤지며 JSON 문서를 작성하죠.

 

게시글(Post)과 댓글(Comment)이 서로를 바라보는 양방향 관계(@OneToMany, @ManyToOne)로 묶여 있다고 상상해 봅시다. 성실한 Jackson 로봇은 JSON을 만들기 위해 아래와 같이 생각합니다.

  1. "오, 1번 게시글이군! 내용을 적어야지. 어? 안에 '댓글 목록'이 있네? 들어가 보자."
  2. "1번 댓글의 내용을 적어야지. 어? 안에 이 댓글이 속한 '게시글' 정보가 또 있네? 다시 들어가 보자."
  3. "오, 1번 게시글이군! 내용을 적어야지. 어? 안에 '댓글 목록'이 있네? 들어가 보자."
  4. "1번 댓글의 내용을... (무한 반복)"

서로가 서로를 호출하는 이 지독한 꼬리 물기가 반복되다가, 결국 서버의 메모리(Stack)가 터져버리며 StackOverflowError가 발생하는 것입니다.

 

2. 해결 방법 ①: 응급처치 (어노테이션 달기)

당장 급하게 에러를 끄고 싶다면, 성실한 Jackson 로봇에게 "이 부분은 더 이상 파고들지 말고 무시해!"라고 안대를 씌워주는 방법이 있습니다.

Entity 클래스에 아래와 같은 어노테이션을 추가하는 것입니다.

  • @JsonIgnore: JSON으로 변환할 때 아예 이 필드를 무시하고 빼버립니다.
  • @JsonManagedReference & @JsonBackReference: 부모 쪽에는 정상적으로 출력하라고 지시하고, 자식(댓글) 쪽에서는 부모(게시글)를 다시 참조하지 않도록 막아줍니다.
Java
 
// Comment (댓글) 엔티티 내부
@ManyToOne
@JoinColumn(name = "post_id")
@JsonIgnore // 💡 "JSON 변환 로봇아, 이 Post 필드는 무시하고 지나가렴!"
private Post post;

[하지만 실무에서는 절대 추천하지 않습니다!] 이 방법은 땜질 처방에 불과합니다. 나중에 "이 화면에서는 댓글 정보가 필요한데요?" 혹은 "저 화면에서는 빼주세요"라는 프론트엔드의 요구가 들어오면 어노테이션만으로는 감당할 수 없게 됩니다.

 

3. 해결 방법 ②: 실무 표준 정석 (DTO 사용하기 - 서빙 접시 비유)

순환 참조 에러가 발생하는 근본적인 원인은 데이터베이스와 찰떡같이 붙어 있는 'Entity' 객체를 함부로 컨트롤러 밖(JSON)으로 바로 던져버렸기 때문입니다.

 

실무에서는 이 문제를 해결하기 위해 DTO(Data Transfer Object, 데이터 전송 객체)라는 전용 서빙 접시를 사용합니다.

Entity DTO 변환 분리 아키텍처 비유
날것의 식재료(Entity)를 손님에게 통째로 주지 마세요. 고객(프론트엔드)이 원하는 정보만 예쁜 접시(DTO)에 담아 전달하면 순환 참조를 완벽히 막을 수 있습니다.

Entity에는 게시글, 댓글, 작성자 정보 등 수많은 정보가 복잡한 거미줄처럼 엮여 있습니다. 이것을 통째로 넘기지 말고, 딱 프론트엔드가 요구하는 데이터만 담을 수 있는 '깔끔한 DTO 클래스'를 새로 만드는 것입니다.

Java
 
// 💡 클라이언트에게 전달할 '전용 접시(DTO)'를 만듭니다.
public class PostResponseDto {
    private Long id;
    private String title;
    private String content;
    // Entity 째로 넣지 않고, 필요한 정보만 String이나 단순 List로 담습니다.
    private List<String> commentContents; 

    // 생성자를 통해 Entity의 내용물 중 필요한 것만 쏙쏙 뽑아 DTO로 옮겨 담습니다.
    public PostResponseDto(Post post) {
        this.id = post.getId();
        this.title = post.getTitle();
        this.content = post.getContent();
        this.commentContents = post.getComments().stream()
                .map(Comment::getContent)
                .collect(Collectors.toList());
    }
}

컨트롤러에서 return post; 대신 return new PostResponseDto(post);를 해주면 어떻게 될까요?

Jackson 로봇은 더 이상 복잡한 Entity의 미로 속을 헤맬 필요 없이, DTO 접시에 담긴 깔끔한 문자열(String)과 숫자들만 읽고 JSON으로 변환을 끝마치게 됩니다. 무한 루프가 완벽하게 차단되는 것이죠!

 

4. 요약: 트러블슈팅 체크리스트

StackOverflowError나 Infinite recursion 에러가 발생했다면 당황하지 말고 아래 사항을 점검하세요.

  1. 에러 원인 파악: 양방향 매핑(@OneToMany, @ManyToOne)이 설정된 Entity를 @RestController에서 그대로 return 하고 있진 않은지 확인합니다.
  2. DTO 분리 적용: 프론트엔드로 반환할 전용 ResponseDto 클래스를 새로 만듭니다.
  3. 데이터 매핑: 컨트롤러나 서비스단에서 Entity를 DTO로 변환하여(필요한 필드만 뽑아서) 반환합니다.

 

마무리

초보 시절에는 귀찮다는 이유로 Entity 객체를 직접 DB에서 꺼내와 API 응답으로 툭 던져버리는 경우가 많습니다. 당장은 편해 보일지 몰라도, 결국 오늘과 같은 끔찍한 무한 루프 에러나 화면에 노출되면 안 되는 민감한 개인정보(비밀번호 등)가 줄줄 새어나가는 보안 사고를 유발하게 됩니다.

 

"Entity는 흙이 묻은 식재료이고, DTO는 손님에게 나가는 예쁜 접시다!"

 

이 철칙 하나만 기억한다면, 여러분은 순환 참조 에러로부터 완벽하게 해방됨은 물론이고 훨씬 더 안정적이고 유지보수하기 좋은 아키텍처를 그리는 개발자로 성장하실 수 있을 것입니다.

 

오늘 준비한 가이드가 여러분의 무한 루프를 끊어내었기를 바랍니다. 막히는 에러가 있다면 언제든 댓글로 남겨주세요.

 

지금까지 데브프리였습니다. 감사합니다!