could not initialize proxy [com.example.jpa_tdd.domain.User#1] - no Session
org.hibernate.LazyInitializationException: could not initialize proxy
[com.example.jpa_tdd.domain.User#1] - no Session
Hibernate에서 lazy loading을 사용하다보면 한 번쯤 직면할 수 있는 문제이다.
(연관관계 매핑에서도 같은 오류가 발생할 수 있지만 여기서는 다루지 않겠다)
먼저 스프링 docs에서는 getOne() 메서드는 더 이상 사용되지 않고 대신 getById()를 사용을 권장하여 getById() 메서드의 스펙을 읽어보았다.
(getOne()과 getById() 메서드의 스펙은 동일)
Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is implemented this is very likely to always return an instance and throw an EntityNotFoundException on first access. Some of them will reject invalid identifiers immediately.
첫 줄부터 reference 즉, 엔티티의 참조를 반환한다고 적혀있다.
기본적으로 Hibernate에서 지연 로딩을 구현하기 위해 실제 객체가 아닌 Proxy 객체를 사용함으로써 실제 객체를 메모리에 로드하지 않아 자원을 절약한다.
getById() 메서드는 지연 로딩을 지원하여 실제 객체가 아닌 Proxy 객체(엔티티의 참조)를 가져오며 실제 객체의 속성에 접근하는 순간 DB에 접근한다.
자세하게는 getById() 메서드는 EntityManager.getReference를 사용하는데 해당 메서드가 프록시 객체를 반환한다.
아래 사진은 getOne() 메서드의 구현체를 나타내며 반환 값에서 em이 EntityManager를 나타내고 getReference()를 통해 프록시 객체를 리턴하는 것임을 알 수 있다.
그래서 오류 메시지를 다시 읽어보자면
could not initialize proxy - no Session
여기서 Session은 영속성 컨텍스트를 관리하는 엔티티 매니저라고 생각하면 된다.
즉, 영속 상태인 Proxy 객체에 실제 데이터를 불러오려고 초기화를 시도하지만 Session이 close되어서 준영속 상태가 되어 값을 가져올 수가 없어 발생한 오류임을 알 수 있다.
다시말해서, 지연 로딩을 하려면 해당 객체는 무조건 영속성 컨텍스트에서 관리해야 한다.
1. @Transaction
그럼 이제 원인을 알았으니 세션을 계속 유지시키기만 하면 된다.
이는 @Transaction 어노테이션으로 해결이 가능하다.
기본적으로 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. (영속성 컨텍스트 생존 범위)
즉, 단위 테스트하는 메서드 위에 @Transaction 어노테이션을 붙이면 해당 메서드가 시작하고 끝날 때까지 같은 세션이 유지 된다.
2. 안티 패턴 2가지
enable_lazy_load_no_trans=true로 설정
spring.jpa.open-in-view : true로 설정 (OSIV)
두 가지 모두 해당 오류를 피하기 위한 임시 방편이라 할 수 있다.
자세한 정보는 아래 링크에서 볼 수 있다.
https://vladmihalcea.com/the-hibernate-enable_lazy_load_no_trans-anti-pattern/
3. findById() 사용하기
즉시로딩(EAGER)을 지원하는 findById()를 사용할 수도 있다.
기능은 동일하며 프록시 객체가 아닌 실제 객체를 반환한다.
아래 사진은 findById() 구현체를 나타내며, 반환 값을 보면 EntityManager에서 직접 객체를 가져오는 Eager 전략을 취하는 것임을 알 수 있다.
꼭 이뤄보고 싶어요 진짜 미친 소스코드에요ㅠㅠㅠ