Backend/Spring

디지몬으로 이해하는 JPA 순환 참조 문제

chillmyh 2025. 6. 6. 18:42

디지몬이랑 테이머는 서로를 끊임없이 참조하ㄴ..

1. 순환 참조란?

JPA에서 엔티티 간 양방향 연관 관계를 설정하면 서로가 서로를 참조하는 구조가 만들어질 수 있다.
이 자체는 잘못된 것은 아니지만, 직렬화(예: JSON 변환)나 무한 출력, 디버깅 시 순환 참조 예외로 이어질 수 있다.

예를 들어 @OneToMany, @ManyToOne 구조를 모두 선언하면 순환이 생길 수 있다.

 

2. 디지몬을 예시로 살펴보자.

디지몬 파트너 구조를 생각해보자.
Tamer(테이머)는 여러 마리의 Digimon(디지몬)을 가질 수 있고,
각 Digimon은 자신이 속한 Tamer를 알고 있다.

 

이 구조는 다음과 같이 모델링할 수 있다.

@Entity
public class Tamer {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "tamer")
    private List<Digimon> digimons = new ArrayList<>();
}

@Entity
public class Digimon {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "tamer_id")
    private Tamer tamer;
}

 

3. 순환 참조가 실제로 발생하는 순간

만약 위 데이터를 API로 제공한다고 가정하자.

@GetMapping("/tamers")
public List<Tamer> getTamers() {
    return tamerRepository.findAll();
}

 

이제 이 요청의 결과를 JSON으로 직렬화할 때 무슨 일이 일어날까?

  1. Tamer → List<Digimon> 접근
  2. 각 Digimon → 다시 Tamer로 접근
  3. 다시 그 Tamer → 또 다른 Digimon으로…

→ 무한 반복
→ StackOverflowError 또는 Jackson의 순환 참조 예외 발생

 

실제로 직렬화 시 이런 문제가 발생한다.

{
  "name": "태일",
  "digimons": [
    {
      "name": "아구몬",
      "tamer": {
        "name": "태일",
        "digimons": [
          {
            "name": "아구몬",
            "tamer": {
              ...
            }
          }
        ]
      }
    }
  ]
}

 

이런 식으로 디지몬과 테이머가 서로를 계속 호출하면서 JSON 트리가 무한히 깊어진다.

 

4. 해결 방법

4.1 @JsonIgnore

가장 간단한 해결책은 디지몬에서 테이머를 직렬화하지 않도록 무시하는 것이다.

@Entity
public class Digimon {
    ...

    @ManyToOne
    @JoinColumn(name = "tamer_id")
    @JsonIgnore
    private Tamer tamer;
}

 

→ 이렇게 하면 디지몬 안에 테이머 정보가 직렬화되지 않는다.

 

장점: 간단하고 직관적
단점: API 응답으로 테이머 정보가 필요한 경우엔 불편함

 

4.2 @JsonManagedReference / @JsonBackReference

Jackson 전용 애노테이션으로 부모-자식 구조를 명시적으로 나누는 방식이다.

public class Tamer {
    ...
    @OneToMany(mappedBy = "tamer")
    @JsonManagedReference
    private List<Digimon> digimons;
}

public class Digimon {
    ...
    @ManyToOne
    @JsonBackReference
    private Tamer tamer;
}

 

  • @JsonManagedReference → 직렬화 대상
  • @JsonBackReference → 직렬화 제외

→ 테이머 → 디지몬까지만 직렬화되고,
→ 디지몬 → 테이머는 출력되지 않는다.

 

4.3 DTO로 변환

가장 근본적인 해결책은 엔티티 자체를 직접 반환하지 않고, DTO로 필요한 정보만 변환해서 전달하는 것이다.

public class TamerDto {
    private String name;
    private List<String> digimonNames;
}

→ 엔티티를 DTO로 매핑해서 필요한 정보만 넣으면 순환 참조는 발생하지 않는다.

 

장점: 직렬화 문제, 보안 문제, 유연성 모두 해결 가능
단점: 매핑 코드가 필요하고 귀찮을 수 있음

 

5. 결론

디지몬 세계관에서, 테이머와 디지몬은 서로를 알고 있어야 하지만
누군가 그대로 출력하게 되면 서로를 무한히 참조하게 된다.

 

무한 참조란 디지몬 세계관에서는 낭만적이지만 실무에서는 처음 맞닥들이면 꽤나 당황스러운 것 같다.

 

해결 방법 특징
@JsonIgnore 빠르고 간단하지만 유연성 부족
@JsonManagedReference / BackReference 구조적 대응이 가능하지만 Jackson에 종속됨
DTO로 변환 가장 확실한 방법. 근본적으로 엔티티 외부 노출을 막을 수 있음