JPA Infinite Recursion 해결하기
JPA Infinite Recursion 해결하기
JPA 사용시 발생하는 Infinite Recursion을 해결하는 방법에 대해 정리한 내용입니다.
1. Infinite Recursion (nested exception) 문제가 발생하는 이유
1.1. 양방향 연관관계에서 발생하는 Infinite Recursion
JPA를 이용하여 양방향 연관관계를 구성한 경우에 발생하는데 컨트롤러에서는 JSON 형태로 값을 출력하기 위해 타입 변환을 필요로 합니다. 이 때 타입 변환이 필요한 엔티티의 필드가 다른 엔티티를 참조하고, 참조한 엔티티의 필드가 기존의 엔티티를 참조하거나 다른 엔티티를 참조하게 되면서 반복적인 재귀가 일어나 Infinite Recursion 문제가 발생하게 됩니다.
다음과 같이 Board, BoardComment 엔티티에 양방향 연관관계를 구성하고 조회해보면 Infinite Recursion 문제가 발생하고 이로 인하여 Jackson 라이브러리에서 JSONObject로 직렬화를 할 때 JsonMappingException 예외가 발생하게 됩니다.
@Getter
@NoArgsConstructor
@Entity(name = "board")
public class Board {
...
@OneToMany(
mappedBy = "board",
cascade = {CascadeType.ALL},
orphanRemoval = true
)
private List<BoardComment> boardComments;
...
}
@Getter
@NoArgsConstructor
@Entity(name = "board_comment")
public class BoardComment {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private Board board;
...
}
ERROR 95745 --- [nio-8081-exec-2] 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.freestrokes.domain.Board["boardComments"]->org.hibernate.collection.internal.PersistentBag[0]
->com.freestrokes.domain.BoardComment["board"]->com.freestrokes.domain.Board["boardComments"]
->org.hibernate.collection.internal.PersistentBag[0]->com.freestrokes.domain.BoardComment["board"])
... ] with root cause
java.lang.StackOverflowError: null
at java.base/java.lang.Exception.<init>(Exception.java:85) ~[na:na]
at java.base/java.io.IOException.<init>(IOException.java:80) ~[na:na]
at com.fasterxml.jackson.core.JacksonException.<init>(JacksonException.java:26) ~[jackson-core-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.core.JsonProcessingException.<init>(JsonProcessingException.java:25) ~[jackson-core-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.DatabindException.<init>(DatabindException.java:22) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.DatabindException.<init>(DatabindException.java:34) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.JsonMappingException.<init>(JsonMappingException.java:247) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:789) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:145) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:107) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:25) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:145) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:107) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:25) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728) ~[jackson-databind-2.13.5.jar:2.13.5]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774) ~[jackson-databind-2.13.5.jar:2.13.5]
...
2. Infinite Recursion 해결하기
2.1. @JsonIgnore 사용하기
다음과 같이 엔티티에서 참조 대상이 되는 필드에 @JsonIgnore 어노테이션을 사용해줍니다. 이렇게 설정하면 Jackson 라이브러리에서 JSONObject 직렬화를 할 때 해당 필드를 제외하게 됩니다. 이 경우에 해당 필드는 null이 들어가기 때문에 DTO 변환 시 필요한 필드를 조회하여 매핑해줘야 합니다.
@Getter
@NoArgsConstructor
@Entity(name = "board")
public class Board {
...
@JsonIgnore
@OneToMany(
mappedBy = "board",
cascade = {CascadeType.ALL},
orphanRemoval = true
)
private List<BoardComment> boardComments;
...
}
2.2. @JsonManagedReference, @JsonBackReference 사용하기
다음과 같이 직렬화를 수행할 필드와 수행하지 않을 필드에 대해 각각 @JsonManagedReference, @JsonBackReference 어노테이션을 설정해줍니다. @OneToMany 필드에 @JsonManagedReference 어노테이션을 설정하여 정상적으로 직렬화를 수행하도록 하고, @ManyToOne 필드에 @JsonBackReference 어노테이션을 설정하여 직렬화 대상에서 제외하도록 합니다.
@Getter
@NoArgsConstructor
@Entity(name = "board")
public class Board {
...
@JsonManagedReference
@OneToMany(
mappedBy = "board",
cascade = {CascadeType.ALL},
orphanRemoval = true
)
private List<BoardComment> boardComments;
...
}
@Getter
@NoArgsConstructor
@Entity(name = "board_comment")
public class BoardComment {
...
@JsonBackReference
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private Board board;
...
}
이상으로 JPA 사용 시 발생하는 Infinite Recursion 예외를 해결하는 방법에 대해 알아봤습니다.
※ References
- www.baeldung.com, Jackson – Bidirectional Relationships, https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion
- imbf.github.io, Jackson Infinite Recursion Issue With JPA Entity, https://imbf.github.io/spring/2020/07/20/Jackson-Infinite-Recursion-Issue-With-JPA-Entity.html
- bellog.tistory.com, [Spring] Could not write JSON: Infinite recursion (StackOverflowError) 이슈, https://bellog.tistory.com/149
- advenoh.tistory.com, Jackson에서 Infinite Recursion 이슈 해결방법, https://advenoh.tistory.com/53
- kim6394.tistory.com, [JPA] Infinite Recursion, https://kim6394.tistory.com/272
- velog.io/@2yeseul, [JPA] Infinite Recursion, https://velog.io/@2yeseul/JPA-Infinite-Recursion