OpenEntityManagerInView (OEIV)는 Spring Framework와 JPA(Java Persistence API)를 함께 사용할 때 발생하는 지연 로딩(Lazy Loading) 문제를 웹 환경에서 해결하기 위해 설계된 패턴이자 Spring에서 제공하는 인터셉터(Interceptor)
1. OEIV의 개념 및 목적
| 목적 | 웹 요청 처리 과정 전체에서 JPA의 EntityManager를 열린 상태로 유지하여, View 렌더링 단계에서 발생하는 지연 로딩 예외(Lazy Loading Exception)를 방지 |
| 작동 방식 | Spring의 DispatcherServlet이 HTTP 요청을 받으면, OpenEntityManagerInViewInterceptor가 트랜잭션과 관계없이 EntityManager를 생성하고 쓰레드 로컬(ThreadLocal)에 바인딩 |
| 종료 시점 | HTTP 응답을 보내기 직전에 EntityManager를 닫고 쓰레드 로컬에서 제거 |
지연 로딩(Lazy Loading) 문제 (N+1 문제와 구별)
일반적으로 JPA는 트랜잭션 경계 내에서만 엔티티의 연관 관계를 로딩(지연 로딩)할 수 있음.
1. Service 계층: EntityManager를 사용하여 데이터를 조회하고 트랜잭션을 종료합니다. (이때 엔티티는 영속성 컨텍스트에서 분리된 상태가 됨)
2. Controller & View 계층: View를 렌더링하는 과정에서 분리된 엔티티의 @ManyToOne이나 @OneToMany 관계 필드를 접근하려고 시도
3. 오류 발생: EntityManager가 이미 닫혔기 때문에, 데이터베이스 접근이 불가능하여 LazyInitializationException이 발생합니다.
OEIV는 이 전체 웹 요청 주기 동안 EntityManager를 열어둠으로써 View 계층에서도 자유롭게 지연 로딩을 수행.
OEIV의 주된 목적은 View 렌더링 과정에서 발생하는 문제를 해결하는 것
* View는 HTML 응답을 만들기 위해 모델 객체의 데이터를 읽어 화면을 꾸미는 단계 전체를 의미
OEIV 미적용 (LazyInitializationException 발생)
“데이터를 가져오려는데 문이 닫혔어요!” (LazyInitializationException)

JPA(Hibernate)를 사용할 때 성능을 위해 연관된 데이터를 바로 가져오지 않고, 실제로 필요할 때 가져오는 지연 로딩(Lazy Loading) 전략을 자주 사용. (예: User 정보를 가져올 때, 그 유저의 Order(주문 목록)는 당장 필요 없으니 나중에 가져오기로 설정).
하지만 기본적으로 데이터베이스 세션(영속성 컨텍스트)은 서비스 계층의 트랜잭션이 끝날 때 함께 닫힘
문제는 컨트롤러가 뷰(HTML 템플릿이나 JSON 직렬화)에 데이터를 넘겨준 후, 뷰가 화면을 그리는 도중에 지연 로딩된 데이터(예: 주문 목록)를 요청할 때 발생. 이때는 이미 데이터베이스 세션의 문이 닫힌 상태라 데이터를 가져올 수 없어 에러가 발생
Service가 끝나는 순간 DB 세션(영속성 컨텍스트)도 같이 죽는다는 점
OEIV 적용
“뷰(View)가 화면을 다 그릴 때까지 데이터베이스 세션(EntityManager)의 문을 열어두자!”

OEIV 패턴을 적용하면 (보통 필터나 인터셉터를 통해 구현), HTTP 요청이 웹 애플리케이션에 들어오는 순간 데이터베이스 세션을 열고, 그리고 컨트롤러, 서비스를 거쳐 뷰가 완전히 렌더링 되고 응답이 나갈 때까지 세션을 닫지 않고 유지
덕분에 뷰 계층에서 지연 로딩된 데이터가 필요해지더라도, 아직 세션이 열려있기 때문에 문제없이 데이터베이스에 추가 쿼리를 날려 데이터를 가져올 수 있음.
Filter 레벨에서 DB 세션을 미리 열고, View 렌더링이 끝날 때까지 닫지 않는 것
2. Spring Boot에서의 설정 및 제어
Spring Boot는 OEIV를 자동으로 설정
| spring.jpa.open-in-view | true | Spring Boot 2.x 기준 기본값 -> OEIV 인터셉터를 활성화 |
설정 방법
application.properties 또는 application.yml 설정
| spring.jpa.open-in-view=true | (기본값) OEIV가 활성화됩니다. View에서 지연 로딩이 가능 |
| spring.jpa.open-in-view=false | OEIV가 비활성화. 지연 로딩은 트랜잭션 경계 내에서만 가능하며, View에서 지연 로딩을 시도시 LazyInitializationException이 발생 |
3. OEIV의 단점 및 주의사항
| 트래픽 부하 및 성능 저하 | HTTP 요청이 끝날 때까지 데이터베이스 연결 세션(Connection)을 붙잡고 있게 되어, 동시 접속자 수가 늘어날 경우 DB Connection Pool의 고갈을 유발 가능 |
| 트랜잭션 오염 | 서비스 계층의 트랜잭션이 종료된 후에도 View 렌더링 과정에서 예상치 못한 지연 로딩으로 추가적인 쿼리가 발생할 가능 (이른바 N+1 문제 심화) |
직접 재현해 본 결과
OEIV 가 있고 없을 때 무엇이 달라지는지 실제로 확인했습니다. Spring Boot 3.5.6 / JDK 21 / H2 환경에서 실행해 확인한 것입니다. 쿼리 수는 Hibernate 의 generate_statistics 를 켜고 PrepareStatementCount 로 측정했습니다.
트랜잭션이 끝난 뒤 프록시를 건드리면
서비스에서 엔티티를 반환하고, 트랜잭션 밖에서 연관을 읽는 구조입니다.
@Service
public class MemberService {
@Transactional(readOnly = true)
public Member findOne(Long id) {
return memberRepository.findById(id).orElseThrow();
}
// 여기서 트랜잭션이 끝난다. 반환된 Member 의 team 은 아직 프록시다.
}
@Test
void lazy_initialization_exception() {
Member m = memberService.findOne(memberId); // 트랜잭션 종료
System.out.println("team 프록시 클래스: " + m.getTeam().getClass().getName());
assertThatThrownBy(() -> m.getTeam().getName())
.isInstanceOf(LazyInitializationException.class);
}
>>> team 프록시 클래스: demo.Team$HibernateProxy
>>> LazyInitializationException 발생 확인
getTeam() 자체는 성공합니다. 프록시 객체를 돌려줄 뿐이기 때문입니다. 예외는 getName() 처럼 실제 값을 요구하는 순간 납니다. 이 시차 때문에 디버깅이 헷갈립니다 — 문제의 원인은 서비스 메서드인데 예외는 컨트롤러나 뷰에서 터집니다.
웹 요청에서는 결과가 갈린다
컨트롤러가 트랜잭션 밖에서 연관을 읽는 코드를 만들고, spring.jpa.open-in-view 값만 바꿔 두 번 실행했습니다.
@RestController
public class MemberController {
@GetMapping("/members/{id}/team-name")
public String teamName(@PathVariable Long id) {
Member m = memberService.findOne(id); // 트랜잭션은 이미 끝났다
return m.getTeam().getName(); // 프록시 초기화 시도
}
}
spring.jpa.open-in-view | 결과 |
|---|---|
true (기본값) | HTTP 200, 본문 team1, 예외 없음 |
false | 실패 — LazyInitializationException: Could not initialize proxy [demo.Team#1] - no session |
같은 코드가 설정 한 줄로 동작이 갈립니다. 이것이 OEIV 가 하는 일입니다 — 영속성 컨텍스트를 요청이 끝날 때까지 열어 둡니다.
그런데 OEIV 는 웹 요청에서만 작동한다
이 점이 자주 오해됩니다. OEIV 는 웹 요청을 감싸는 인터셉터로 구현돼 있습니다. 따라서 웹 요청 스코프 밖에서는 아무 도움이 되지 않습니다.
위 첫 번째 테스트가 그 증거입니다. @SpringBootTest에서 서비스를 직접 호출했을 때는 OEIV 가 기본값 true였는데도 LazyInitializationException이 났습니다. 웹 요청이 아니었기 때문입니다.
같은 이유로 다음 경우들에는 OEIV 가 통하지 않습니다.
@Scheduled 배치 작업 -> 웹 요청이 아니다
@Async 비동기 메서드 -> 요청 스레드가 아니다
메시지 컨슈머 (Kafka/RabbitMQ) -> 웹 요청이 아니다
테스트 코드에서의 서비스 직접 호출 -> 웹 요청이 아니다
OEIV 에 기대는 코드는 웹 계층에서만 동작하는 코드가 됩니다. 같은 서비스 메서드를 배치에서 재사용하는 순간 예외가 납니다. 이게 OEIV 를 끄라고 권하는 실질적인 이유입니다 — 커넥션 점유 시간보다 계층이 웹에 묶이는 것이 더 큰 문제입니다.
Spring Boot 도 경고를 띄운다
애플리케이션을 띄우면 로그에 이 경고가 나옵니다. 기본값이 true인데도 프레임워크가 스스로 주의를 주는 흔치 않은 경우입니다.
WARN JpaBaseConfiguration$JpaWebConfiguration :
spring.jpa.open-in-view is enabled by default. Therefore, database queries may be
performed during view rendering. Explicitly configure spring.jpa.open-in-view to
disable this warning
경고를 없애려면 값을 명시하라고 합니다. 끄든 켜든 의식적으로 정하라는 뜻입니다.
# 어느 쪽이든 명시하는 것이 맞다
spring.jpa.open-in-view=false
끄기로 했다면 연관 데이터는 트랜잭션 안에서 미리 로딩해야 합니다. fetch join이나 @EntityGraph로 필요한 것만 가져오고, 엔티티가 아니라 DTO 로 반환하는 것이 정석입니다. 쿼리 수를 실제로 세어 본 비교는 Lazy vs Eager 글에 정리해 두었습니다.
“OEIV : OpenEntityManagerInView”에 대한 2개의 생각