JPA

Infinite recursion 에러

dev-rootable 2024. 1. 21. 11:48

📌 에러 내용

 

ERROR 21512 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] :
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);  //원인
nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)
(through reference chain: com.rootable.libraryservice2022.domain.Book$HibernateProxy$yexDv6MB["postsList"]
->org.hibernate.collection.internal.PersistentBag[0]
->com.rootable.libraryservice2022.domain.Posts["book"]
->com.rootable.libraryservice2022.domain.Book$HibernateProxy$yexDv6MB["postsList"]
-> ...(반복)
->org.hibernate.collection.internal.PersistentBag[0]
->com.rootable.libraryservice2022.domain.Posts["book"]
->com.rootable.libraryservice2022.domain.Book$HibernateProxy$yexDv6MB["postsList"])] with root cause

 

📌 상황

 

HttpClient 측에서 GET 요청을 하여 서버 측에서 데이터를 내려주는 상황
해당 데이터는 Entity 목록으로 다른 Entity와 연관 관계를 맺고 있음 

 

📌 원인

 

다음은 문제가 되는 Posts 엔티티다.

 

@Getter
@Entity
@SequenceGenerator(
        name = "POSTS_SEQ_GENERATOR",
        sequenceName = "POSTS_SEQ",
        initialValue = 1, allocationSize = 1
)
@NoArgsConstructor
public class Posts extends BaseTimeEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "POSTS_SEQ_GENERATOR")
    @Column(name = "post_id")
    private Long id;

    @Column(length = 20, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String content;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    private Member member; //게시물 작성자

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "book_id", nullable = false)
    private Book book; //신청 도서

    private Long fileId;

    @Enumerated(EnumType.STRING)
    private Result result;

    @OneToMany(mappedBy = "posts", cascade = CascadeType.ALL)
    @OrderBy("id asc") //댓글 정렬
    private List<Comment> commentList = new ArrayList<>();

    @Builder
    public Posts(Long id, String title, String content, Member member, Book book, Long fileId, Result result) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.member = member;
        this.book = book;
        this.fileId = fileId;
        this.result = result;
    }

    /*
     * 연관관계 메서드
     * */
    ...

}

 

LAZY 로딩을 통해 연관 엔티티의 초기화를 DB 조회까지 미루지만, 엔티티를 JSON으로 변경하는 Serialize(직렬화) 과정을 거치기 때문에 연관 관계를 맺은 엔티티를
참조하게 된다.

 

이때 상대편 엔티티도 마찬가지로 연관 관계 필드가 있으므로 다시 반대쪽 엔티티를 참조하면서 무한 재귀가 발생하고, 이로 인해 StackOverflow가 발생한 것이다.

 

📌 해결 방법

 

🔍 @JsonIgnore

 

가장 간단한 방법으로 이것을 붙인 연관 관계 필드는 직렬화에서 제외된다.

 

@Getter
@Entity
@SequenceGenerator(
        name = "POSTS_SEQ_GENERATOR",
        sequenceName = "POSTS_SEQ",
        initialValue = 1, allocationSize = 1
)
@NoArgsConstructor
public class Posts extends BaseTimeEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "POSTS_SEQ_GENERATOR")
    @Column(name = "post_id")
    private Long id;

    @Column(length = 20, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String content;

    @JsonIgnore
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    private Member member; //게시물 작성자

    ...

    /*
     * 연관관계 메서드
     * */
    ...

}

 

위 코드처럼 @JsonIgnore를 붙여주면 Member 필드는 직렬화에서 제외된다.

 

단점은 크게 2가지가 있다. 먼저, 이렇게 제외할 필드마다 @JsonIgnore를 붙여줘야 하기 때문에 반복되는 애노테이션이 많아지고 반복 코드는 유지보수에도
좋지 못하다. 또 하나는 연관 관계를 맺은 데이터가 필요하다면 다른 방법을 고려해야 한다. 그 이유는 직렬화에서 제외된다는 것은 클라이언트로 전달되지 않는다는
의미
이기 때문이다.

 

🔍 @JsonManagedReference과 @JsonBackReference

 

  • @JsonManagedReference: OneToMany에 사용
  • @JsonBackReference: ManyToOne에 사용

 

위와 비슷한 방법으로 @JsonManagedReference와 @JsonBackReference가 있다.

 

연관 관계가 어떤 관계인지에 따라 다르게 사용되기 때문에 @JsonIgnore보다 가독성이 좋은 장점이 있다. 하지만 마찬가지로 반복해서 애노테이션을 사용해야 하고, @JsonBackReference가 붙은 필드는 직렬화에서 제외된다.

 

@Getter
@Entity
@SequenceGenerator(
        name = "POSTS_SEQ_GENERATOR",
        sequenceName = "POSTS_SEQ",
        initialValue = 1, allocationSize = 1
)
@NoArgsConstructor
public class Posts extends BaseTimeEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "POSTS_SEQ_GENERATOR")
    @Column(name = "post_id")
    private Long id;

    @Column(length = 20, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String content;

    @JsonBackReference
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    private Member member; //게시물 작성자

    @JsonBackReference
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "book_id", nullable = false)
    private Book book; //신청 도서

    private Long fileId;

    @Enumerated(EnumType.STRING)
    private Result result;

    @JsonManagedReference
    @OneToMany(mappedBy = "posts", cascade = CascadeType.ALL)
    @OrderBy("id asc") //댓글 정렬
    private List<Comment> commentList = new ArrayList<>();

    @Builder
    public Posts(Long id, String title, String content, Member member, Book book, Long fileId, Result result) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.member = member;
        this.book = book;
        this.fileId = fileId;
        this.result = result;
    }

    /*
     * 연관관계 메서드
     * */
    ...

}

 

🔍 DTO 사용

 

DTO를 사용하는 이유이기도 하다. 엔티티를 바로 반환하지 않고 DTO로 감싸서 클라이언트 응답으로 주는 것이다.

 

//Service측

public List<DTO> findAll() {
    List<Entity> entities = repository.findAll(); //repository => DI로 받은 DAO
    List<DTO> DTOs = new ArrayList<>();
    
    for (Entity entity : entities) {
        DTOs.add(new DTO(entity.getXXX, entity.getYYY)); //필요한 데이터만
    }
    
    return DTOs;
}

 

위와 같은 방식으로 작성하여 에러를 해결할 수 있었다.

 

필자의 경우, Posts 엔티티와 Member의 이름이 필요하여 @JsonManagedReference/@JsonBackReference와 DTO 모두 사용했다.

 

📝 체크
👉 LAZY 도 직렬화를 수행한다.
👉 REST API에서 커스텀 데이터를 전달하고 싶다면 DTO를 활용하자

 

Refernces:

https://ahn3330.tistory.com/164

 

[JPA] 스프링부트 Could not write JSON: Infinite recursion 에러 해결

에러 내용 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 recur

ahn3330.tistory.com

 

https://velog.io/@2yeseul/JPA-Infinite-Recursion

 

[JPA] Infinite Recursion

오류 코드 원인 JPA 연관관계에서 양방향 매핑을 선언한 경우 발생 컨트롤러에서 JSON으로 값을 출력하는 경우, 타입을 변환해야 하는데 변환되는 엔티티의 필드가 다른 엔티티를 참조하고 또 그

velog.io

 

https://kim6394.tistory.com/272#google_vignette

 

[JPA] Infinite Recursion

JPA Infinite Recursion JPA를 활용하여 양방향 매핑을 선언했을 때 아래와 같은 에러가 발생할 수 있다. org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.http.co

kim6394.tistory.com