SW 개발/Spring

Spring Boot, JPA, LazyInitializationException 예외 설명 및 해결책 정리

지단로보트 2022. 7. 4. 11:28

개요

  • Spring Boot, JPA 환경에서 개발하다보면 JPA의 멋진 철학에 매료되었다가 곧 현실과의 괴리를 느끼고, 끊임없는 내적 갈등에 휩싸이게 된다. 그런 상황을 유발하는 대표적인 예외가 바로 LazyInitializationException인데 이번 글에서는 해당 예외가 발생하는 이유와 해결책을 정리하였다.

LazyInitializationException 예외가 발생하는 이유

  • 일반적인 백엔드 프로젝트에서 로직의 대부분은 REST API에 해당하며 실행의 흐름은 @Controller, @Service, @Repository 순서가 된다.
  • 서비스 레벨에서 @Transactional이 명시된 메써드가 종료되면 HibernateSession도 함께 종료된다.
  • FetchType.LAZY가 설정된 필드가 포함된 엔티티 오브젝트에 대해, 컨트롤러 레벨에서 해당 필드를 조회할 때 Getter 메써드를 호출하고 실제 조회 쿼리가 실행된다. 하지만 앞서 이미 Session이 종료된 상태이기 때문에 LazyInitializationException 예외가 발생하게 되는 것이다.

해결책이나 안티 패턴인 방법

  • 미리 안티 패턴 임을 밝히는 첫번째 해결책은, @Transactional이 명시된 메써드가 종료되어도 Session을 컨트롤러의 응답 리턴 시점까지 유지시키는 것이다. Spring의 환경 변수를 아래와 같이 설정하면 된다. 하지만 Session의 수명이 길어지기 때문에 권장되지 않는다.
spring.jpa.open-in-view=true (기본값: false)
  • 역시 안티 패턴에 해당하는 두번째 해결책은, Session이 종료되었더라도 예외를 발생시키지 말고 다른 Session을 사용하여 데이터를 조회하는 것이다. Spring의 환경 변수를 아래와 같이 설정하면 된다. 엔티티에 설정된 관계의 복잡성과 상황에 따라 커넥션 풀을 고갈시키는 장애를 유발할 수 있어 권장되지 않는다. (기본값으로 예외를 발생시키는 것에는 이유가 있다.)
spring.jpa.properties.hibernate.enable_lazy_load_no_trans = true (기본값: false)

전문가들이 권장하는 해결책

  • 전문가들이 가장 권장하는 해결책은, 서비스 레벨에서 트랜잭션이 종료되는 시점에 리턴 타입으로 엔티티를 DTO로 변환하는 것이다. 하나의 엔티티를 단일 DTO로 변환하는 것은 FetchType.EAGER를 사용하는 것과 다를 바 없으므로 API의 응답을 세분화하여 필요한 상황에 맞는, 필요한 필드만 각 DTO로 맵핑하여 리턴하는 것이다. (예를 들면 getUser(), getUserWithXXX()와 같이 세분화하는 것이다. 이를 통해 퍼포먼스의 이점도 누릴 수 있다.) 더 적극적으로 나아가면 @Repository에서 데이터를 조회하는 시점부터 즉시 DTO에 대한 프로젝션을 수행하도록 설계하는 것이다. (High-Performance Java Persistence의 저자인 Vlad Mihalcea가 이 방법을 추천하고 나 또한 동의한다.)
  • 마지막으로 내가 권장하는 해결책은, 엔티티 조회 레벨에서 바로 DTO로 프로젝션한다는 개념은 동일하지만, 그 프로젝션 행위의 수단으로서 Querydsl JPA를 사용하는 것이다. 본 블로그에 이 글에 자세히 설명하였다.

참고 글