JPA에서의 순환 참조 이해와 해결 방법
1. JPA 순환 참조
공모전에서 관심 멘토를 추가하는 API를 만들던 중에 순환 참조 문제를 만났습니다. stackoverflow: null이라는 메시지가 떴는데 왠지 모르게 친숙했습니다. 하지만 친숙한 느낌과는 다르게 이전 프로젝트에서는 겪은 적이 없었던 문제라 해결하는 데에 많이 애먹었습니다. 왜 이전 프로젝트에서 겪은 적이 없었는지는 나중에 알게됩니다. ㅎㅎ;
JPA란?
Java Persistence API(JPA)는 Java ORM 기술인 Hibernate를 기반으로 만든 API로, 객체 지향적인 프로그래밍을 지원하고, SQL과 JDBC를 사용하는 데 필요한 공통적인 코드를 줄여줍니다. 하지만 JPA를 사용하다보면 종종 순환 참조(Circular Reference) 문제와 마주칠 수 있습니다.
순환 참조
순환 참조란 두 객체가 서로를 참조하는 상황을 의미합니다. 이는 일대일, 일대다, 다대다 관계를 가진 엔티티를 다룰 때 발생하는 문제입니다. 순환 참조가 발생하면 자바의 가비지 컬렉터(GC)는 더 이상 접근할 수 없는 객체를 식별하지 못하게 되고, 메모리 누수가 발생할 수 있습니다. 또한 순환 참조는 JSON 직렬화 시에 무한 루프를 유발할 수 있고 서버의 성능 저하를 일으킬 수 있습니다.
2. 순환 참조의 발생 원인
공모전 프로젝트의 Profile 엔티티와 Favorite 엔티티 클래스를 기준으로 설명하겠습니다.
@Entity
public class Profile {
// ... 다른 필드들
@OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Favorite> favorites = new HashSet<>();
// ... 다른 필드 및 getter, setter, method
}
@Entity
public class Favorite {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id")
private Profile profile;
// ... 다른 필드 및 getter, setter, method
}
Profile은 여러 개의 Favorite을 가지고 있고, Favorite은 하나의 Profile을 가지고 있습니다. 이러한 관계로 인해, Profile을 조회할 때 관련된 Favorite 목록도 같이 조회되고, 이 때 각 Favorite이 참조하는 Profile이 다시 조회되는 순환 참조가 발생합니다.
Profile의 Set<Favorite> favorites 조회 → Favorite 객체로 들어가자 Profile이 있음 → 다시 Profile의 Set<Favorite> favorites 조회 → ... 가 무한 반복!
즉, 엔티티에 구현되어 있는 내용이기 때문에 엔티티를 직접 REST API에서 조회하는 경우 순환 참조가 발생하게 됩니다.
3. 순환 참조 해결 방법
@JsonManagedReference
,@JsonBackReference
어노테이션 사용: Jackson 라이브러리에서 제공하는 이 어노테이션을 사용하여 순환 참조를 제어할 수 있습니다.@JsonIgnore
사용: 순환 참조가 발생하는 필드에 이 어노테이션을 사용하여 JSON 직렬화에서 해당 필드를 무시하도록 합니다.@JsonSerialize
사용: 특정 필드를 원하는 방식으로 직렬화하는 커스텀 직렬화를 정의할 수 있습니다.- ✅ DTO(Data Transfer Object) 사용: 엔티티 대신 전송에 필요한 정보만 담은 DTO를 사용하여 순환 참조를 제거합니다.
4. DTO를 이용한 해결 방법
도입부에서 이전에는 겪어본 적이 없다고 했습니다. 그 이유는 그 당시 자연스럽게 보여주고 싶은 정보만 담기 위해서 DTO를 사용했기 때문입니다. 그래서 이번에도 위에서 언급한 방법 중 DTO를 이용한 방법으로 순환 참조 문제를 해결했습니다.
DTO를 사용하면 필요한 데이터만 클라이언트에게 제공할 수 있고, 순환 참조 문제를 회피할 수 있습니다.
public class ProfileResponse {
// ... 다른 필드들
private Set<FavoriteResponseDto> favorites;
}
public class FavoriteResponseDto {
private Long id;
private Long profileId;
private Long mentorId;
}
이렇게 FavoriteResponseDto를 사용하면 ProfileResponse 객체를 조회할 때 FavoriteResponseDto 목록을 조회하도록 하면 FavoriteResponseDto가 Profile 객체를 가지고 있지 않기 때문에 순환 참조 문제를 해결할 수 있습니다.
이 방식의 단점을 굳이 꼽자면 DTO를 관리하는 것은 추가적인 코드를 작성해야 하므로 복잡성이 증가할 수 있습니다. 하지만 순환 참조로 서버에서 기능을 못 사용하는 것보다는 낫겠죠?
결론
JPA를 사용하면서 순환 참조 문제를 해결하는 과정에서 느낀점은 역시나 알고 사용해야 한다는 점입니다. 그리고 순환 참조 문제가 꽤나 빈번한 문제임을 알게 됐습니다. 더 빨리 알았더라면 하는 생각도 드네요. ㅎㅎㅎ
오늘은 여러 가지 해결 방법 중에서 DTO를 사용하는 방법을 소개하였으며, 이 방법을 통해 순환 참조 문제를 해결할 수 있었습니다. 혹시나 제가 틀린 부분 또는 질문이 있으시다면 편하게 말씀해주세요. 읽어주셔서 감사합니다!