슬기로운 개발생활

[JPA] 양방향 순환참조 문제 및 해결방법

by coco3o
반응형

게시판 프로젝트를 진행하던 중 순환 참조 문제에 마주하였다.

※ 순환 참조(Circular reference)란, 참조하는 대상이 서로 물려 있어 참조할 수 없게 되는 현상을 말한다.

 

JPA에서 양방향으로 연결된 Entity를 그대로 조회하는 경우 서로의 정보를 순환하면서 조회하다가 stackoverflow가 발생하게 된다.

※ Spring Boot는 @ResponseBody(rest api)를 구현할 시 Object를 JSON 형태로 변환하기 위해 Jackson 라이브러리를 이용하는데,

Jackson은 entity의 getter를 호출하고, 직렬화를 이용해 JSON 형태로 객체를 변화시키고 view로 전달하는데

getter를 호출하는 과정에서부터 순환 참조가 계속 발생해 view로 전달하면서 stackoverflow가 발생하게 된다.

※ 직렬화란, 객체의 내용을 바이트 단위로 변환하여 파일 또는 네트워크를 통해 스트림(송수신)하도록 하는 것을 의미한다.


다음 코드는 게시글과 댓글의 Entity 클래스이다.

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Entity
public class Posts extends TimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    ...
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @OneToMany(mappedBy = "posts", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
    private List<Comment> comments;

    ...
}
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Table(name = "comments")
@Entity
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    ...
    
    @ManyToOne
    @JoinColumn(name = "posts_id")
    private Posts posts;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user; // 작성자
}

Posts Entity에서 Comment를 부르는 부분을 보면 다음과 같다.

@OneToMany(mappedBy = "posts", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
private List<Comment> comments;

Posts에서 Comment는 연관관계의 주인이 아니기 때문에 데이터베이스에서 따로 FK로 저장되진 않는다.

하지만 JPA는 서버에서 클라이언트로 Posts의 정보를 보내줄 때 댓글 정보도 같이 보내주게 된다.

 

반대로, Comment Entity에서 Posts를 부르는 부분을 보자.

@ManyToOne
@JoinColumn(name = "posts_id")
private Posts posts;

Comment는 연관관계의 주인이므로 데이터베이스에 Posts에 대한 "posts_id"를 FK로 갖고 있다.

이 또한 JPA가 서버에서 클라이언트로 Comment의 정보를 보내줄 때 Posts 정보도 같이 보내주게 된다.

 

REST API로  Entity를 컨트롤러에서 직접조회하는 경우 다음과 같은 결과를 초래한다.

Posts > Comment > Posts > Comment와 같이 서로를 계속해서 참조하게되어 순환 참조를 하는 것이다.


순환 참조를 방지하기 위한 방법은 여러 가지가 있다.

 

1. @JsonIgnore 

: 이 어노테이션을 붙이면 JSON 데이터에 해당 프로퍼티는 null로 들어가게 된다.

즉, 데이터에 아예 포함시키지 않는다.

 

2. @JsonManagedReference 와 @JsonBackReference

: 부모 클래스(Posts entity)의 Comment 필드에 @JsonManagedReference를, 자식 클래스(Comment entity)의 Posts 필드에 @JsonBackReference를 추가해주면 순환 참조를 막을 수 있다.

 

3.@JsonIgnoreProperties

: 부모 클래스(Posts entity)의 Comment 필드에 @JsonIgnoreProperties({"posts"}) 를 붙여주면 순환 참조를 막을 수 있다.

 

4. DTO 사용

: 위와 같은 상황이 발생하게된 주원인은 '양방향 매핑'이기도 하지만, 더 정확하게는 Entity 자체를 response로 리턴한데에 있다. entity 자체를 return 하지 말고, DTO 객체를 만들어 필요한 데이터만 옮겨담아 Client로 리턴하면 순환 참조 관련 문제는 애초에 방지 할 수 있다.

 

5. 매핑 재설정

: 양방향 매핑이 꼭 필요한지 다시 한번 생각해볼 필요가 있다. 만약 양쪽에서 접근할 필요가 없다면 단방향 매핑을 해줘서 자연스레 순환 참조 문제를 해결하자.

 

필자는 DTO 사용으로 순환 참조 문제를 해결했다.

이번을 계기로 DTO를 사용해야 하는 이유를 하나 더 알게되었다.

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기