본문 바로가기

Tech/Spring

[Spring] Spring Data JPA OSIV와 지연 로딩

개요

 Todo 어플리케이션에서 마감 시간 1시간 전인 Todo에 대해 사용자에게 알림을 발송하는 기능을 구현 했습니다. 매 분 0초 마다 스케줄러가 데이터베이스에서 1시간 뒤 종료되는 Todo를 조회하도록 했고, 반환된 Todo의 사용자에게 SSE 알림을 발송하도록 했습니다.

 

LazyInitializationException

 

 그런데 Controller에서 응답을 위해 Todo 엔티티를 DTO로 변환하는 과정에서 문제가 발생했습니다. 문제 발생 원인은 다음과 같습니다.

 

  1. OSIV 설정을 false로 지정했다.
  2. Todo에서 User 매핑을 설정할 때 fetchType.LAZY를 적용했다.
  3. @Transactional 범위를 넘어가는 Controller에서 Todo가 참조하는 User Proxy 객체에 접근 하니 LazyInitializationException 예외가 발생했다.

 

이제 왜 지연 로딩 예외가 발생했는지 알아보도록 하겠습니다.

 

OSIV란

  • OSIV는 Open Session In View의 약자입니다.
  • Session은 ORM 기술 표준 JPA의 Entity Manager를 뜻합니다. Entity Manager는 DB 커넥션을 점유하여 DB와 통신해 엔티티 객체를 관리합니다.
  • View는 일반적으로 HTTP 요청, 응답 과정 중 응답을 생성하는 단계, View 렌더링 단계를 의미합니다.

 즉 OSIV란 HTTP 응답을 생성하는 단계에서 Entity Manager(스프링 부트와 DB 사이의 커넥션)을 유지할지 말지의 여부를 결정하는 것입니다.

 

HTTP 요청 흐름으로 알아보는 OSIV 특징

스프링 HTTP 요청 흐름

 일반적으로 스프링에서 HTTP 요청 처리 흐름은 다음과 같습니다.

 

 

여기서 OSIV가 True 라면 엔티티 매니저가 HTTP 요청 전반에 걸쳐 생존하게 됩니다. 반면 False 라면 엔티티 매니저가 @Transactional, 즉 트랜잭션이 시작되고 종료되는 Service와 Repository 범위에서 생존합니다.

 

Todo와 fetchType.LAZY로 매핑된 User는 todo.getUser() 처럼 직접적으로 접근해야 실제 데이터가 로딩이 되는데, 이는 엔티티 매니저가 유효한 범위에서나 가능한 이야깁니다.

 

따라서 LazyInitializationException이 발생했던 원인은 OSIV 설정이 false라서 트랜잭션의 범위를 넘어설 경우 Entity Mangager가 생존하지 않기 때문이었습니다. @Transactional의 유효 범위를 벗어난 Controller에서 프록시 객체 User에 접근했기 때문에, 예외가 발생했던 것이었습니다.

 

OSIV true, false 장단점

  1. OSIV true
    • 장점: 웹 요청 전반에 걸쳐 Entity Manager가 생존하기 때문에 코드의 어디에서든지 간단히 프록시 객체의 지연로딩이 가능합니다.
    • 단점: 하지만 웹 요청 전반에 걸친 DB 커넥션 점유는 곧 커넥션 고갈로 이어질 수 있어 주의해야 합니다.
  2. OSIV false
    • 장점: @Transactional의 유효 범위인 Service와 Repository에서 Entity Manager가 생존합니다. DB 커넥션 점유 시간이 짧아 성능 상 좋습니다.
    • 단점: 하지만 트랜잭션 범위(Entity Manager의 생존 범위) 안에서 프록시 객체의 지연로딩을 모두 완료해야 하기 때문에 코드 상 복잡해질 수 있습니다.

 

OSIV 권장 설정

  • OSIV는 false를 설정하고 연관관계 매핑은 fetchType.LAZY를 적용하자. 매핑된 엔티티가 함께 필요한 기능에는 트랜잭션 안에서 지연로딩 하거나, fetch join을 통해 즉시 로딩하는 전략을 취하자.
  • 만약 Websocket 또는 SSE 통신을 할 경우 OSIV를 반드시 fasle로 설정하자.

 OSIV를 true로 할 경우 DB 커넥션 고갈이 우려되고, 처음 부터 연관 매핑이 된 객체를 함께 조회하는 fetchType.Eager를 적용하면 select 성능이 우려됩니다. 따라서 fetchType.LAZY로 매핑하되, 매핑한 엔티티가 반드시 필요한 로직의 경우 트랜잭션 범위 안에서 지연 로딩을 해주거나 fetch join으로 함께 조회하는 전략을 취하는 것이 좋습니다.

 

 또한 HTTP 커넥션이 끊어지지 않고 지속적으로 유지되는 WebSocket 또는 SSE 통신의 경우, 엔티티 매니저가 지속적으로 생존해 DB 커넥션을 점유하게 됩니다. 따라서 OSIV를 false로 설정하는 것이 좋습니다.

 

Reference