[JPA] LazyInitializationException

서론 

프로젝트 진행 중 로컬환경에서는 예외가 발생하지 않았지만, 배포 환경에서는 LazyInitializationException이 발생했고 둘의 차이점을 확인해 보니 로컬은 OSIV 즉 Open Session In Veiw를 true로 설정하고 배포 환경에는 적용이 안된 것을 확인했다. 이를 통해 스프링 컨테이너에서 적용되는 영속성 컨텍스트의 범위에 대해 알아보자.

 

스프링 컨테이너의 전략

스프링은 트랜잭션 범위의 영속성 컨텍스트 전략을 사용하는데, 이는 트랜잭션이 실행되면 영속성 컨텍스트가 생성되고, 종료되면 그전에 플러시를 통해 DB와 동기화한 후에 DB 트랜잭션 커밋을 수행한 뒤 영속성 컨텍스트를 종료한다.

 

스프링을 통해 개발을 하면 주로 서비스 로직의 메서드에 트랜잭션을 적용하게 된다. 이는 서비스 로직 시작 전 @Transactional을 통해 영속성 컨텍스트를 생성하고 로직이 다 끝난 뒤 메서드가 종료되면 영속성 컨텍스트도 종료한다.

 

준영속과 지연로딩

영속성 컨테스트가 종료되고 엔티티가 컨트롤러에 넘어가면 준영속 상태가 되고 해당 상태에서는 변경감지가 동작하지 않는다. 또한, 지연로딩으로 인해 생성된 프록시 객체를 조회하고자 할 때 LazyInitializationException가 발생한다.

이런 불편함은 추후에 프레젠테이션 영역인 View, Controller 영역에서 지연로딩을 사용할 수 없는 단점을 야기한다.

 

불편함 해결방법

만약에 프래젠테이션 영역에서 지연로딩 객체가 필요할 때 사용하는 여러 방법이 존재한다.

글로벌 패치 조인

해당 방법은 지연 로딩을 즉시 로딩으로 수정하는 방법이다. 이 방법은 여러 단점이 존재한다.

  1. 필요하지 않을 때도 엔티티를 로딩한다.
  2. 즉시로딩으로 인해 추후에 N+1의 문제를 야기한다.
  3. 여러 개를 즉시로딩하면 멀치 패치 조인으로 인한  MultipleBagFetchException가 발생한다.

2, 3번에 대해서는 해결방법이 존재하지만 글로벌 패지 조인은 요구사항에 맞게 잘 결정하는 게 좋다고 생각한다.

 

DTO를 이용한 강제 초기화

트랜잭션이 유지되는 서비스 메서드 안에서 지연 로딩을 통해 프록시 객체를 초기화시키고 DTO 응답 시 필요한 정보들을 DTO 객체에 담아서 넘겨주는 방법이 있다.

 

FACADE 계층 추가

서비스 앞에 FACADE 계층을 추가해서 FACADE에 @Tranactional을 추가한 뒤 영속성 컨텍스트의 범위를 늘린 뒤 FACADE에서 지연로딩을 사용하는 방법이 있다. 해당 방법은 계층이 하나가 더 늘어나는 것이기에 코드가 많이 증가한다.

 

OSIV 해결책

OSIV란?

OSIV(Open Session In View)란 영속성 컨텍스트를 View까지 유지하겠다는 의미를 가지고 있다.

과거의 OSIV

과거에는 요청이 들어오는 순간부터 트랜잭션이 시작되고 영속성 컨텍스트가 생성되었고, 요청이 종료되면 끝나는 방식을 취했다. 이는 View에서도 지연로딩을 가능하게 했다. 

과거의 OSIV의 문제점

영속성 컨텍스트가 View까지 유지되다 보니 View에서 엔티티를 수정하면 추후 View를 반환한뒤 트랜잭션이 종료될때 수정된 데이터가 DB에 연동되는 문제가 발생한다. 비즈니스 로직에서 데이터의 변동이 발생해야하는데 View에서 발생하면 심각한 문제를 야기할 수 있다.

 

개선된 스프링 OSIV

과거 OSIV는 문제점이 있었지만 스피링의 OSIV는 해당 문제점을 어느 정도 해결했다. 기존과 같이 요청이 들어오면 영속성 컨텍스트가 생성되고, 요청이 끝나면 영속성 컨텍스트가 종료되지만 트랜잭션은 @Transactional을 입력한 비즈니스 로직에만 유지하는 것이다. 트랜잭션 내에서만 엔티티를 업데이트할 수 있고 트랜잭션 밖은 엔티티를 조회만 할 수 있다.

 

물론 스프링 OSIV도 요청이 끝나기 전에 또 다른 트랜잭션을 실행시키고 그곳에서 이전에 조회한 엔티티를 수정하면 실제로 적용되는 부분도 존재하지만 이런 부분은 거의 발생하지 않는다고 보면 된다.