[Spring] 스프링 순환 참조, 무한 재귀 해결하기 (DTO, JsonIgnore)
jpa 엔티티 일대다 : 다대일 관계 생성시 흔하게 일어나는 순환 참조문제,
이전 프로젝트에서는 뭣도 모르고 @jsonignore로 처리했었는데, 이게 왜 일어나는지 공부해보니, 좋지 않은 방법이였다는 걸 깨달았다,..
JPA에서 순환참조 문제는 이제 알고보니까는, 엔티티를 조회할 떄 발생하는 게 아니라
entity를 json으로 변환할 떄, 즉, Entity To Json serialize할 때 (보통 controller 단)에서 일어나는 문제였다.
Entity를 Json으로 변환하면서, 연관된 객체를 다시 Json으로 변환하고, 이 변환 과정에서, 다시 연관된 객체의 연관된 객체를 참조... 참조 지옥,,, 무한 참조,,, 크기가 펑펑펑,,
스프링 무한 순환 참조 문제 해결 방법
@jsonignore (지양함)
- 간단하게 순환참조를 해결할 수 있지만, 연관관계 복잡하게 얽혀있으면 문제가 많음 (사용해야할 떄, 사용하지 못하기도 함)
- 어노테이션이 붙은 객체를 Json 직렬화를 하지 않음
DTO (지향)
- 무한 순환 참조는 객체를 JSON 형태로 변환하는 직렬화 과정에서 발생
- JSON으로 직렬화 할 객체에 연관 관계의 필드를 넣지 않음으로 문제 해결 가능
- 즉, DTO에, 필요한 필드만 만들어서 담아, 순환참조가 일어나지 않도록 하자
무한 순환 참조 에러 코드
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError);
이런식으로 계속 뭔가 참조되는 볼수 있다.
(StackOverflowError) (through reference chain: org.hibernate.collection.internal.PersistentBag[0]->com.sparta.weeklytestspring.domain.PostComment["user"]->com.sparta.weeklytestspring.domain.User["posts"]->org.hibernate.collection.internal.PersistentBag[0]->com.sparta.weeklytestspring.domain.Post["comments"]->org
Post 엔티티
package com.sparta.weeklytestspring.domain;
import com.sparta.weeklytestspring.dto.SetArticleDto;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@AllArgsConstructor
@NoArgsConstructor@Setter
@Getter
@Builder
@Entity
@Where(clause = "deleted_at IS NULL")
@SQLDelete(sql = "UPDATE post SET deleted_at = CURRENT_TIMESTAMP where id = ?")
public class Post extends Timestamped {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long idx;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
//게시글에 달린 댓글 목록
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
private List<PostComment> comments;
//게시글에 달린 좋아요 목록
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
private List<PostLike> postLikes = new ArrayList<>();
//작성한 유저
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="user_idx", nullable = false)
private User user;
}
엔티티를 바로 참조함(모든 필드를)
package com.sparta.weeklytestspring.dto;
import com.sparta.weeklytestspring.domain.PostComment;
import com.sparta.weeklytestspring.domain.PostLike;
import com.sparta.weeklytestspring.domain.UserRole;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
@Setter
@Getter
public class GetPostDto {
@Getter
@Setter
public static class Response {
private Long idx;
private String title;
private String content;
private List<PostComment> comments;
private List<PostLike> postLikes;
private User user;
private LocalDateTime createdAt;
}
}
참조하는 엔티티의 필요한 부분만, 클래스로 만들어 순환참조가 일어나지 않도록 함
package com.sparta.weeklytestspring.dto;
import com.sparta.weeklytestspring.domain.PostComment;
import com.sparta.weeklytestspring.domain.PostLike;
import com.sparta.weeklytestspring.domain.UserRole;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
@Setter
@Getter
public class GetPostDto {
@Getter
@Setter
public static class Response {
private Long idx;
private String title;
private String content;
private List<Comment> comments;
private List<Like> postLikes;
private User user;
private LocalDateTime createdAt;
}
@Getter
@Setter
public static class Comment {
private Long idx;
private String comment;
}
@Getter
@Setter
public static class Like {
private Long idx;
private String comment;
}
@Getter
@Setter
public static class User {
private String username;
private UserRole account_type;
}
}
*참고
https://yeon-kr.tistory.com/170