본문 바로가기

프로젝트일지

[Inside] failed to lazily initialize a collection of role 해결

개발 도중 로그인 후에 아래와 같은 오류가 발생했다.

 

 

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: unannn.inside.domain.user.User.applications, could not initialize proxy - no Session

 

loginUser.getApplications()를 호출할 때 LazyInitializationException 발생

원인을 찾아보니, 한 트랜잭션에서  생성된 엔티티 객체에서 트랜잭션이 종료된 후 지연로딩을 통해 연관 데이터에 접근하려고 했기 때문이라고 한다. 그리고 대부분의 글에서 연관관계에 있는 엔티티의 fetch = FetchType.LAZY 설정을 FetchType.EAGER 로 변경, 즉 지연로딩을 즉시 로딩으로 바꿔서 문제를 해결하라고 말했다.

 

하지만 이러한 방식은 User 테이블을 조회할 때마다 불필요한 조인을 발생시킨다. 현재 User 테이블에선 아래에서처럼 두 엔티티와 일대다 연관관계를 맺고 있기 때문에 지연 로딩을 유지하고 싶었다.

 

 

 

 

그러다 비슷한 문제를 겪고있는 아래 글을 보게 되었다.

https://www.inflearn.com/questions/33949

 

failed to lazily initialize a collection of role 오류 관련 문의 - 인프런 | 질문 & 답변

안녕하세요. 항상 친절하고 정확한 답변 감사합니다. 지금 작업하다가 발생한 오류로 헤매고 있어서 질문드립니다. 소셜로그인으로 로그인 시 기존 회원정보가 없으면 생성하고 나서 소셜로그

www.inflearn.com

 

결국 LazyInitializationException 는 영속성 컨텍스트에 존제하지 않는 엔티티에서 지연로딩을 하려고 했기 때문에 발생한 문제이기 때문에 @Transactinal 을 통한 트랜잭션 설정을 통해 문제를 해결하라는 것이었다.

 

하지만 현재 개발하고 있는 프로젝트에서 User 엔티티를 조회하는 트랜잭션은 로그인할 때 발생하고, 이는 Spring Security에 의해서 관리된다. 그리고 이때 조회한 User 엔티티 객체를 다른 비즈니스 로직에서 사용할 때 사용하므로 @Transactional을 통해 같은 영속성 콘텍스트에 관리하기는 힘들었다.

 

때문에 처음에는 아래와 같이 User 엔티티 객체가 같고 있는 id를 활용해 다 엔티티 객체를 조회하는 방식으로 영속화하여 개발해야하나 생각했지만, 멍청한 생각이었다.

이미 조회하여 세션에 존재하는 엔티티 객체를 다시 조회하는 비효율적인 로직이기 때문이다. 때문에 어떻게 이문제를 해결할까 고민하다가, 엔티티가 식별자인 id를 갖고 있다는 점에서 준영속 개념이 생각났다.

 

준영속이란, 영속화가 되었던 엔티티가 영속성을 잃은 상태를 말한다. 이렇게 준영속 상태 엔티티의 특징은 비영속과 다르게 '식별자'를 갖고있다는 점이다.

 

현재 발생한 문제도 결국 findByUsername()에 의해 조회할 때 영속화 되었던 User 엔티티 객체가 조회 이후에 트랜잭션이 종료되어 영속성 컨텍스트가 사라지면서 준영속화 되었고, 그 상태에서 지연로딩을 시도했으니 발생한 문제였다.

 

결국 관건은 준영속 상태의 User 엔티티를 다시 영속화 시켜주는 것이었다.

 

이를 위해 EntityManager를 생성자 주입을 통해 주입받고, em.merge() 메소드를 통해 준영속 상태의 엔티티를 영속화 시켜주었다.

em.merge()를 통해 준영속상태의 User 엔티티를 영속화

 

이렇게 하니 결국 더 이상 LazyInitializationException 에러는 발생하지 않고, 정상적으로 지연로딩이 실행되었다.

 

 

+ 추가

공부 하면서, Spring Data JPA 안에서 EntityManager를 주입받아 사용하고 있고,  이를 활용해 엔티티를 영속화 시켜주는 것이 일관성을 지키고, 코드의 가독성 측면에서 더 나을 것 같다는 생각이 들었다.

 

JpaRepository를 상속받은 Repository에서는 save 메소드를 통해 엔티티를 영속화 시키고, 메소드 안에는 준영속 상태의 엔티티를 영속화 시키는 로직 또한 포함되어 있다.

 

아래는 save 메소드의 내부 모습이다.

JpaRepository 구현객체 SimpleJpaRepository의 save() 메소드

만약 엔티티가 영속화 된적 없는 새로운 엔티티라면 persist()를 통해, 그리고 준 영속 상태라면 위에서 직접 했던 것 처럼 merge()를 통해 영속화 시켜준 다는 것을 알 수 있다. 변경 코드는 아래와 같다.

 


결론

원인

트랙잭션이 끝나 준영속 상태의 엔티티에서 지연로딩을 통해 다른 엔티티를 조회하려 했기 때문에

 

 

해결 방법

생성자 주입을 통해 EntityManager 빈을 주입받고, 지연로딩이 발생하는 트랙잭션 안에서 em.merge()를 통해 준영속된 엔티티를 영속화 후 지연로딩 조회 실행

 

 

느낀점

JPA를 공부하면서 비영속, 영속화, 준영속 등에 대해 이해하고 있다고 생각했다고 생각했는데, 프로젝트를 진행하면서 관련 문제를 만나니 공부한 내용이 쉽게 떠오르지 않았다. 역시 공부만 한다고 실력이 느는건 절대 아닌 것 같고, 공부한 내용을 바탕으로 프로젝트를 하면서 발생하는 여러 문제 해결 경험을 쌓아야 실력이 늘겠다고 느꼈다.