Skip to content

Latest commit

 

History

History
153 lines (122 loc) · 10.9 KB

File metadata and controls

153 lines (122 loc) · 10.9 KB

06. OSIV와 예외 변환

무엇을 / 왜

이 챕터는 spring-orm의 두 가지 횡단 관심사를 다룬다.

  1. OSIV (Open EntityManager In View) — 트랜잭션은 서비스 계층에서 끝나지만, 뷰 렌더링 시점에 지연 로딩(lazy loading)된 연관을 접근하면 LazyInitializationException이 난다. OSIV는 요청 전체 동안 EntityManager를 열어 두어 뷰에서도 영속성 컨텍스트를 쓸 수 있게 한다.
  2. 예외 변환 — JPA/Hibernate가 던지는 PersistenceException(unchecked) 계층을 Spring의 일관된 DataAccessException 계층으로 바꾼다. 이로써 데이터 접근 기술에 독립적인 예외 처리가 가능하다.

관련 타입은 support 패키지의 OpenEntityManagerInViewFilter/Interceptor/AsyncRequestInterceptor와, 예외 변환 쪽의 EntityManagerFactoryUtils·PersistenceExceptionTranslator 구현들이다.

핵심 타입

 [OSIV]
 ┌────────────────────────────────┐  ┌──────────────────────────────┐
 │ OpenEntityManagerInViewFilter  │  │ OpenEntityManagerInView-      │
 │  (Servlet Filter,              │  │ Interceptor                   │
 │   OncePerRequestFilter 상속)   │  │  (HandlerInterceptor,         │
 │  요청 단위로 EM 바인딩/해제    │  │   Spring MVC용)               │
 └────────────────────────────────┘  └──────────────────────────────┘
              │ 둘 다 사용
              ▼
 ┌────────────────────────────────┐
 │ AsyncRequestInterceptor        │  비동기(Callable/DeferredResult)
 │  요청 중단/재개 시 EM 바인딩    │  처리 시 EM 누수 방지
 │  관리                          │
 └────────────────────────────────┘

 [예외 변환]
 ┌──────────────────────────────────────────────────────────┐
 │ PersistenceExceptionTranslator (spring-tx의 SPI)          │
 │   translateExceptionIfPossible(RuntimeException)          │
 │     → DataAccessException 또는 null                       │
 └───────────────┬──────────────────────────────────────────┘
                 │ implements
   ┌─────────────┼────────────────────────┐
   ▼             ▼                         ▼
 AbstractEntity  JpaDialect /          HibernateException-
 ManagerFactory  DefaultJpaDialect     Translator
 Bean            └─ EntityManager-      (Hibernate 전용)
 (그 존재만으로     FactoryUtils.
  변환 활성화)      convertJpaAccess-
                   ExceptionIfPossible()

동작 흐름 1 — OSIV 필터의 요청 생명주기

OpenEntityManagerInViewFilter.doFilterInternal()(소스 130행~)은 요청 처리 전후로 EM을 바인딩/해제한다.

 요청 도착
   │
   ├─ EMF 조회 (lookupEntityManagerFactory)
   │     init-param: entityManagerFactoryBeanName / persistenceUnitName
   │     기본 빈 이름 "entityManagerFactory"
   │
   ├─ 이미 EMF가 스레드에 바인딩돼 있나? (participate 판단)
   │     YES → 아무것도 안 함 (중첩 OSIV / 이미 트랜잭션이 EM 보유)
   │     NO  ↓
   │
   ├─ EntityManager em = emf.createEntityManager()
   │     EntityManagerHolder로 감싸 bindResource(emf, holder)
   │     (비동기 대비 AsyncRequestInterceptor 등록)
   │
   ├─ filterChain.doFilter(...)   ← 컨트롤러 + 뷰 렌더링 전체
   │     이 구간에서 서비스의 @Transactional이 같은 EM을 발견해 재사용
   │     (JpaTransactionManager.doGetTransaction이 holder를 찾음)
   │
   └─ finally:
        participate 아니면 unbindResource(emf)
        비동기 시작 안 됐으면 closeEntityManager(em)

핵심은 OSIV가 묶어 둔 EM을 JpaTransactionManager가 "pre-bound holder"로 인식한다는 점이다. 04장에서 본 대로, 트랜잭션 매니저는 기존 holder를 발견하면 새 EM을 만들지 않고 그것을 재사용하며(doBegin), 트랜잭션이 끝나도 닫지 않는다(doCleanupAfterCompletion에서 newHolder만 close). 그래서 트랜잭션 종료 후에도 EM이 살아 있어 뷰에서 지연 로딩이 가능하다. 최종적으로 EM을 닫는 책임은 필터의 finally에 있다.

participate 분기가 중첩을 막는다. 이미 EMF가 바인딩돼 있으면(다른 OSIV나 외부 트랜잭션) 새로 만들지 않고, 해제도 자기가 만든 경우에만 한다.

동작 흐름 2 — 비동기 요청에서의 EM 관리

Spring MVC가 Callable/DeferredResult를 반환하면 요청 스레드가 반환되고 처리가 다른 스레드에서 이어진다. 이때 EM을 그냥 풀면 비동기 작업 중에 EM이 닫혀 버린다. AsyncRequestInterceptor가 이를 조율한다(필터 소스 170~178행에서 등록).

 컨트롤러가 Callable 반환
   → 요청 스레드: 비동기 시작됨 → 필터 finally에서 close 안 함
                  (isAsyncStarted(request) 체크)
   → AsyncRequestInterceptor가 EM holder를 들고 있다가
     비동기 처리 스레드에서 다시 바인딩, 완료 시 정리

이로써 비동기 디스패치 전체에서 EM이 일관되게 유지된다. OpenEntityManagerInViewInterceptor도 동일한 원리를 MVC HandlerInterceptor로 구현하며, applyEntityManagerBindingInterceptor()로 비동기 재바인딩을 처리한다.

동작 흐름 3 — JPA 예외 → DataAccessException 변환

변환의 중심은 EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible()(소스 457행)이다. JPA 표준 예외를 Spring 예외로 매핑한다.

 JPA 예외 (RuntimeException)
   │
   ├─ IllegalStateException / IllegalArgumentException
   │       → InvalidDataAccessApiUsageException
   ├─ EntityNotFoundException     → JpaObjectRetrievalFailureException
   ├─ NoResultException           → EmptyResultDataAccessException
   ├─ NonUniqueResultException    → IncorrectResultSizeDataAccessException
   ├─ QueryTimeoutException       → (spring) QueryTimeoutException
   ├─ LockTimeoutException        → CannotAcquireLockException
   ├─ PessimisticLockException    → PessimisticLockingFailureException
   ├─ OptimisticLockException     → JpaOptimisticLockingFailureException
   ├─ EntityExistsException       → DataIntegrityViolationException
   ├─ TransactionRequiredException→ InvalidDataAccessApiUsageException
   ├─ 그 외 PersistenceException  → JpaSystemException
   └─ 그 외 (사용자 코드 예외)    → null  (변환 안 함, 원본 전파)

마지막 null 반환이 설계의 묘다(소스 501~504행). PersistenceException 계층이 아닌 예외는 데이터 접근 계층이 아니라 사용자 코드에서 났다고 보고 변환하지 않는다. 그래야 비즈니스 예외가 엉뚱하게 DAO 예외로 둔갑하지 않는다.

JpaDialect(=PersistenceExceptionTranslator)는 이보다 정밀한 변환을 제공한다. HibernateExceptionTranslator는 Hibernate JDBCException의 SQL 상태/에러코드까지 분석해 더 구체적인 DataAccessException을 만든다.

동작 흐름 4 — 변환은 어떻게 "자동" 활성화되는가

AbstractEntityManagerFactoryBeanPersistenceExceptionTranslator를 구현한다(02장). Spring의 PersistenceExceptionTranslationPostProcessor가 컨텍스트에서 이 인터페이스를 구현한 빈(=EMF 팩토리빈)을 자동 감지하고, @Repository가 붙은 빈에 AOP 어드바이스를 적용한다.

 @Repository 빈의 메서드에서 PersistenceException 발생
   │
   ├─ PersistenceExceptionTranslationInterceptor가 가로챔
   ├─ 컨텍스트의 PersistenceExceptionTranslator들에게 변환 요청
   │     → AbstractEntityManagerFactoryBean.translateExceptionIfPossible()
   │         JpaDialect 있으면 그것으로, 없으면 convertJpaAccess...()
   └─ DataAccessException으로 바꿔 다시 던짐

LCEMFB 빈을 정의하기만 하면 예외 변환 인프라가 켜진다(소스 83~89행 주석). 트랜잭션 경로에서는 별도로 JpaTransactionManager.doCommit/doRollback이 직접 JpaDialect로 변환한다(04장).

핵심 메서드 정리

메서드 위치 하는 일
doFilterInternal() OpenEntityManagerInViewFilter 요청 동안 EM 바인딩, 끝에 해제/close
lookupEntityManagerFactory() 빈 이름/PU 이름으로 EMF 조회
convertJpaAccessExceptionIfPossible() EntityManagerFactoryUtils JPA 표준 예외 → DataAccessException
translateExceptionIfPossible() AbstractEntityManagerFactoryBean dialect 우선, 없으면 표준 변환

설계 포인트 / 확장점

  • OSIV의 trade-off: 편리하지만 DB 커넥션/영속성 컨텍스트를 뷰 렌더링까지 잡고 있어, 고부하 환경에서는 커넥션 보유 시간이 길어진다. 그래서 명시적 설정(필터/인터셉터 등록)을 요구하는 opt-in 기능이다.
  • null 반환 = "내 소관 아님": 변환기들이 변환 불가 시 null을 반환하는 규약 덕에 여러 변환기를 체인으로 시도할 수 있고, 비-DAO 예외를 안전하게 통과시킨다.
  • 2단 예외 계층: 루트 org.springframework.orm의 기술 독립 예외(ObjectRetrievalFailureException 등)를 orm.jpa의 JPA 구현 예외(JpaObjectRetrievalFailureException 등)가 상속한다(01장). 변환 결과는 항상 이 공통 계층으로 수렴한다.
  • 확장점: OSIV 필터의 createEntityManager(emf)를 오버라이드해 프로퍼티를 주거나, 커스텀 PersistenceExceptionTranslator 빈을 추가해 변환 규칙을 확장할 수 있다.

정리

OSIV는 요청 전체 동안 EntityManager를 스레드에 묶어 두어 뷰에서의 지연 로딩을 가능하게 하며, JpaTransactionManager가 이 pre-bound EM을 재사용하고 닫지 않는 규약과 맞물려 동작한다. 예외 변환은 convertJpaAccessExceptionIfPossible()가 JPA 표준 예외를, JpaDialect/HibernateExceptionTranslator가 벤더 예외를 Spring DataAccessException 계층으로 바꾸며, EMF 팩토리빈이 PersistenceExceptionTranslator를 구현하므로 @Repository에 대해 자동으로 활성화된다. 두 기능 모두 데이터 접근 기술 세부를 애플리케이션 코드에서 가리는 것이 목적이다.