Spring/Spring Boot

[Spring] 스프링 순환 참조, 무한 재귀 해결하기 (DTO, JsonIgnore)

민돌v 2022. 4. 20. 18:13

 

 

 

 

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