Entity를 Json으로 변환하다보면 lazy loading된 객체를 제대로 serialize하지 못하는 이슈가 있다.
예제 코드를 통해 문제를 확인해보자.
@Entity
class Article(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
var body: String,
@OneToMany(mappedBy = "article")
var comments: List<Comment>
)
@Entity
class Comment(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
var body: String,
var createdAt: LocalDateTime,
@ManyToOne
@JoinColumn(name = "article_id")
var article: Article? = null
)
Test
fun beforeTest() {
val article = Article(body = "first article", comments = mutableListOf())
articleRepository.save(article)
val comments = (1 until 3).map {
Comment(body = "comment body $it", createdAt = LocalDateTime.now(), article = article)
}
commentRepository.saveAll(comments)
}
@Test
fun `entity serialize test`() {
val article = articleRepository.findByIdOrNull(1)
?: throw EntityNotFoundException()
jacksonObjectMapper().writeValueAsString(article)
}
위 테스트를 실행해보면 아래와 같은 결과가 나온다.
내용을 읽어보면 세션이 없어서 프록시로부터 lazy loading하는 객체를 못가져오는것으로 보인다.
실행결과
Hibernate: select article0_.id as id1_0_0_, article0_.body as body2_0_0_ from article article0_ where article0_.id=?
failed to lazily initialize a collection of role: io.freddie.dddstudy.article.entity.Article.comments, could not initialize proxy - no Session (through reference chain: io.freddie.dddstudy.article.entity.Article["comments"])
com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: io.freddie.dddstudy.article.entity.Article.comments, could not initialize proxy - no Session (through reference chain: io.freddie.dddstudy.article.entity.Article["comments"])
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:397)
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:356)
article을 조회하고나서 이미 세션이 닫히고, 그 이후 proxy를 통해 접근하려니까 문제가 발생한 경우이다.
사실 테스트코드 같은 경우는 @Transactional
을 설정해서 동작하도록 할 수 있다.
@Transactional
이 설정되면 해당 트랜잭션이 종료될때까지 세션은 살아있고, 그 안에서 json으로 변환하는 경우 정상적으로 lazy loading이 가능해지기 때문이다.
하지만, Controller의 응답으로 내려보내야 하는 경우에는 세션을 계속 열어두기 부담스러울 수 있다 (Open Session In View설정으로 가능할 수 있다.)
특히 article에 대한 응답만 내려주고 comments의 내용이 필요하지 않은 경우에는 select를 다시 실행해서 json을 만들어줄 필요도 없다.
이런 경우 Hibernate5Module
을 사용할 수 있다.
이 모듈은 lazyLoading설정으로 아직 불러오지 않은 엔티티에 대해 null값을 내려주는 모듈이다.
아래 의존성을 추가해서 다시 테스트를 진행해보자.
implementation("com.fasterxml.jackson.datatype:jackson-datatype-hibernate5")
Test
@Test
fun `entity serialize test`() {
val article = articleRepository.findByIdOrNull(1)
?: throw EntityNotFoundException()
println(jacksonObjectMapper().registerModule(Hibernate5Module())
.writeValueAsString(article))
}
실행결과
Hibernate: select article0_.id as id1_0_0_, article0_.body as body2_0_0_ from article article0_ where article0_.id=?
{"id":1,"body":"first article","comments":null}