본문 바로가기

Tech/JPA

[JPA] LazyInitializationException 과 OSIV

반응형

테스트 코드를 작성하다가 LazyInitializationException 에러가 발생하였다. 이슈에 대해서 찾아보니 JPA 의 lazy loading 과 관련된 이슈였으며, @Transactional 등을 사용하여 해결할 수 있었다. 오늘은 해당 이슈가 발생하고 이를 해결하면서 가졌던 의문점과 이에 대한 설명을 정리해본다.

1. LazyInitializationException

에러 설명 및 발생 원인

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.msa.dataset.domain.entity.Project.categories: could not initialize proxy - no Session
 at org.hibernate.collection.spi.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:634)
 at org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217)
 at org.hibernate.collection.spi.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:613)
 at org.hibernate.collection.spi.PersistentSet.add(PersistentSet.java:189)
 at com.msa.dataset.domain.entity.Project.addCategory(Project.java:53)

 

일대다 연관관계를 가지는 엔티티에 대해서 테스트 코드를 작성하다가 위와 같은 에러가 발생하였다. 이름에서 LazyInitializationException 이기 때문에 당연히 연관관계, lazy loading 과 관련있을 것이라는 생각을 했다.

 

로그를 확인해보니 "could not initialize proxy - no Session" 이라고 되어있다. JPA 에서 lazy loading 을 사용하여 엔티티를 조회하면 주체 엔티티만 실제 객체로 조회하고 연관관계 엔티티는 실제 객체가 아닌 proxy 객체로 조회된다. proxy 객체로 조회된 엔티티는 코드에서 proxy 객체에 접근할 때 조회 쿼리를 발생하여 실제 엔티티를 가져온다. 근데 로그를 보면 no Session 이라고 출력된다. 이를 통해 DB 와 연결된 세션이 없어서 proxy 엔티티를 조회할 수 없어서 발생하는 에러라는 것을 유추할 수 있다.

LazyInitializationException 해결법

해결법은 간단했다. DB 세션이 유지되지 않아서 발생하는 이슈이기 때문에 @Transactional 어노테이션을 로직이 수행되는 메서드에 사용해주면 된다. @Transactional 어노테이션을 사용하면 해당 로직이 하나의 트랜잭션으로 수행된다. 이때문에 @Transactional 어노테이션을 붙이면 해당 메서드 내에서는 LazyInitializationException 이 발생하지 않는다.

 

eager loading 을 사용하거나 이나 join fetch 를 사용하여 한번에 연관된 엔티티들을 조회하도록 하는 방법도 있으나 이 방법들은 현재 상황에 맞는 방법이 아니어서 설명에서 제외하였습니다.

의문점

그런데 이 이슈를 처리하면서 의문점이 생겼다. 바로 테스트 코드에서만 LazyInitializationException 에러가 발생한다는 것이다.

 

이전에 JPA 공부를 하면서 OSIV 에 대해서 본 적이 있다. OSIV 기능을 사용하면 view 렌더링 시점까지 엔티티의 영속성을 유지할 수 있다. 그렇기 때문에 스프링 애플리케이션을 실행해서 해당 로직을 호출하고 엔티티를 다뤘을 때는 LazyInitializationException 에러가 발생하지 않는다는 것이었다. 이때문에 당연히 테스트 코드에서도 동일한 방식으로 구현하였는데, 테스트 코드에서는 LazyInitializationException 이 발생하니 굉장히 의문이었다.

 

마침 이러한 의문과 비슷한 문제로 고민하신 분의 블로그를 통해서 문제의 실마리를 찾을 수 있었다. 이 분은 배치 프로그램을 구현하다가 LazyInitializationException 에러가 발생하여 이 해결법과 OSIV 의 연관관계에 대해서 정리해놓으셨다.

 

- https://veluxer62.github.io/explanation/osiv/

 

왜 LazyInitializationException이 발생하지? - OSIV편

이 글은 Spring Data JPA를 이용하여 배치 기능을 개발하면서 겪었던 LazyInitializationException 발생 사례를 토대로 문제 해결방법을 찾아가는 과정을 적어본 내용이다. 사실 이 글에서 중점적으로 다루

veluxer62.github.io

2. OSIV

OSIV Open Session In View 의 줄임말로 스프링에서 Open EntityManager In View 패턴을 적용하여 lazy loading 이 웹 뷰에서도 유효할 수 있도록 해주는 기능이다. 이 기능은 application.properties 에서 spring.jpa.open-in-view 속성의 값을 true 또는 false 로 설정하여 사용할 수 있다. 기본값은 true 이다.

 

spring.jpa.open-in-view=false

OSIV 와 web request

스프링 공식문서의 Open EntityManager in View 항목을 보면 OSIV 에 대한 설명이 나와있다. 설명 글에 따르면 웹 애플리케이션을 실행하면 스프링부트가 자동으로 OpenEntityManagerInViewInterceptor 을 등록하여 Open EntityManager in View 패턴을 적용한다고 되어있다. 이로 인해서 웹 뷰에서 lazy loading 을 사용할 수 있게된다.

 

OpenEntityManagerInViewInterceptor 는 Spring web request interceptor 로 요청이 들어오면 해당 요청을 처리하는 쓰레드에 JPA EntityManger 를 바인딩한다. 이를 통해서 이미 트랜잭션이 완료된 후에도 웹 뷰에서 lazy loading 을 사용할 수 있도록 해준다.

 

이러한 설명들을 통해서 OSIV 는 웹 요청에 대해서만 유효한 것을 확인할 수 있다. 이때문에 웹 요청에서는 LazyInitializationException 이 발생하지 않고 테스트 코드에서만 발생하는 것이다.

OSIV 와 @Transactional

스프링에서 트랜잭션을 관리하기 위해 @Transactional 어노테이션을 많이 사용한다. 이 어노테이션이 붙은 메서드가 호출되면 자동으로 JPA EntityManager 를 할당하고 해당 메서드를 하나의 트랜잭션 단위로 인식하여 동작한다. 메서드가 종료되면 자동으로 commit 또는 rollback 을 수행하고 세션을 종료한다. 그렇다면 OSIV 와 @Transactional 이 공존하는 경우에 세션은 어떻게 관리될까?

 

@Transactional 함수가 호출되면 Transaction manager 에서 새로운 트랜잭션의 생성 여부를 판단한다. 새로운 트랜잭션을 생성해야 하는 경우 새로운 EntityManager 를 생성하고 이를 현재 스레드에 할당한 후 DB connection 을 할당한다. 하지만 OSIV 가 활성화 되어 있는 경우에는 이미 요청이 들어온 시점에 세션이 생성되어 있기 때문에 Transaction manager 에서 새로운 세션을 생성하도록 하지 않고 이미 생성된 세션을 사용하도록 한다. 이때문에 OSIV 이 활성화 되어있으면 @Transactional 가 선언된 메서드 밖에서도 엔티티를 사용할 수 있는 것이다.

[Reference]

- https://veluxer62.github.io/explanation/osiv/

 

왜 LazyInitializationException이 발생하지? - OSIV편

이 글은 Spring Data JPA를 이용하여 배치 기능을 개발하면서 겪었던 LazyInitializationException 발생 사례를 토대로 문제 해결방법을 찾아가는 과정을 적어본 내용이다. 사실 이 글에서 중점적으로 다루

veluxer62.github.io

- https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#data.sql.jpa-and-spring-data.open-entity-manager-in-view

 

Spring Boot Reference Documentation

This section goes into more detail about how you should use Spring Boot. It covers topics such as build systems, auto-configuration, and how to run your applications. We also cover some Spring Boot best practices. Although there is nothing particularly spe

docs.spring.io

- https://dev-monkey-dugi.tistory.com/167

 

JPA의 캐시와 @Transactional(readOnly=true) OSIV의 관계

1차 캐시 와 스냅샷, 2차 캐시 스냅샷 영속성 컨텍스트가 생성될 때, 향후 변경 감지를 위해서 원본을 복사해서 만들어둔 객체 1차 캐시 단순히 엔티티를 캐싱해두고 같은 key이면 디비로 재요청

dev-monkey-dugi.tistory.com

- https://www.baeldung.com/spring-open-session-in-view

- https://www.codepedia.org/jhadesdev/how-does-spring-transactional-really-work/

 

How does Spring @Transactional Really Work?

Share coding knowledge

www.codepedia.org

 

반응형

'Tech > JPA' 카테고리의 다른 글

[JPA] JPQL 경로표현식과 fetch join  (0) 2022.06.19
[JPA] JPQL 기본 문법  (0) 2022.06.18
[JPA] 값 타입  (0) 2022.06.04
[JPA] 영속성 전이 (CASCADE)  (0) 2022.05.26
[JPA] 프록시와 지연로딩  (0) 2022.05.23