entity json변환시 proxy 이슈

freddie·2021년 4월 17일
1

Jpa공부

목록 보기
2/3

문제상황

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}

결론

  • 되도록이면 화면으로 결과를 내려줄때는 DTO를 사용하자
  • Session In View를 화면단까지 세션이 열려있기 때문에 프록시 관련 문제는 해결된다.(진짜 사용이 필요한지 다시한번 확인하자)
  • 꼭 같이 내려보내야 하는 내용이 아니라면 모듈을 사용하자.
profile
하루에 하나씩만 배워보자

0개의 댓글