테스트 코드를 작성하다가 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/
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/
- https://dev-monkey-dugi.tistory.com/167
- https://www.baeldung.com/spring-open-session-in-view
- https://www.codepedia.org/jhadesdev/how-does-spring-transactional-really-work/
'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 |