OEIV : OpenEntityManagerInView

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-viewtrueSpring Boot 2.x 기준 기본값 -> OEIV 인터셉터를 활성화

설정 방법

application.properties 또는 application.yml 설정

spring.jpa.open-in-view=true(기본값) OEIV가 활성화됩니다. View에서 지연 로딩이 가능
spring.jpa.open-in-view=falseOEIV가 비활성화. 지연 로딩은 트랜잭션 경계 내에서만 가능하며, 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개의 생각

댓글 남기기