BackEnd/JPA

[JPA] JSON 직렬화 순환 참조 해결하기

샤아이인 2022. 6. 5.

 

오늘도 프로젝트를 구현하던 도중에 거의 매번 만나왔던 순환 참조 문제를 만나게 되었다.

이번이 처음은 아니지만, 한번쯤 정리해볼만한 내용인것 같아 정리해둔다.

 

1. JSON에서 순환 참조 문제

▶ 순환 참조

JPA에서 양방향으로 연결된 엔티티를 JSON 형태로 직렬화하는 과정에서, 서로의 정보를 계속 순환하며 참조하여StackOverflowError 를 발생시키는 현상이다.

 

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

이때 Jackson은 entity의 getter를 호출하여 필드에 접근하고, 직렬화를 이용해 Object를 JSON 형태로 객체를 변화시켜 반환한다.

 

문제는 getter를 호출하는 과정에서부터 순환 참조가 계속 발생하게 되어 stackoverflow가 발생하는 것 이다.

 

 

▶ 직렬화

객체/데이터를 바이트 형태로 변환하여 네트워크를 통해 송수할 수 있도록 만드는 것

 

2. 예시

Posts 와 Comment 가 1:N의 상황이다.

@NoArgsConstructor
@AllArgsConstructor
@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;  
    
    ...
}
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "comments")
@Getter
@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에서 Comments 를 호출할 때 다음 collection부분을 통해 가져오게 된다.

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

만약 우리가 posts를 JSON으로 반환한다고 해보자!

 

posts를 직렬화 -> posts 내부의 List<Comment> comments 직렬화  -> 각 comment들의 내부에 있는 posts 를 직렬화 (무한순환반복)

 

이렇게 무한하게 직렬화 하려 출력하다가 StackOverflow 나고 끝나버린다.

 

3. 해결 방안

3-1) @JsonManagedReference & @JsonBackReference

양방향 관계에서 직렬화 방향을 설정하여 순환 참조를 해결할 수 있도록 설계된 annotation 즉 이 에노테이션들은 만들어진 목적 자체가 순환참조를 제거하기 위함이다.

 

@JsonManagedReference

  • 연관관계 주인 반대 Entity 에 선언
  • 정상적으로 직렬화 수행
@JsonManagedReference
@OneToMany(mappedBy = "posts", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)    
private List<Comment> comments;

 

@JsonBackReference

  • 연관관계의 주인 Entity 에 선언
  • 직렬화가 되지 않도록 수행
@JsonBackReference
@ManyToOne
@JoinColumn(name = "posts_id")    
private Posts posts;

 

3-2) @JsonIgnore 

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

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

 

3-3) DTO 사용

위와 같은 상황이 발생하게된 주원인은 '양방향 매핑'이기도 하지만, 더 정확하게는 Entity를 직접 반환한것도 문제이다.

entity 자체를 return 하지 말고, DTO 객체를 만들어 필요한 데이터만 반환하면 순환 참조 관련 문제를 사전에 방지 할수 있다.

 

 

댓글