brunch

You can make anything
by writing

C.S.Lewis

by anonymDev May 15. 2023

코드로 보는 스프링부트 OSIV 동작 원리

OSIV와 EntityManager LifeCycle

OSIV가 무엇인지 간단하게 알아보자


OSIV는 Open-Session-In-View Pattern의 약어이다. OSIV 패턴은 트랜잭션(Transaction)이 외부에서 엔티티(Entity)의 지연 로딩(Lazy Loading) 기능을 제공한다. 트랜잭션이 종료된 후에 엔티티매니저(EntityManager)가 닫히게 되면 기본적으로 지연 로딩이 제공되지 않는다.

하지만 spring.jpa.open-in-view 를 true로 설정하면 트랜잭션이 종료된 후에도 엔티티 지연 로딩이 가능하다.


spring-boot과 OpenEntityManagerInViewInterceptor


spring-boot autoconfiguration 기능을 활용하면 OSIV 기능을 간편하게 활용할 수 있다.

spirng-boot를 open-in-view 설정을 활성화하면 JpaWebConfiguration이 활성화되어

openEntityManagerInViewInterceptor 빈이 WebRequestInterceptor에 추가된다.


(코드)
@ConditionalOnProperty(prefix = "spring.jpa", name = "open-in-view", havingValue = "true", matchIfMissing = true)
protected static class JpaWebConfiguration {
...

/**

* openEntityManagerInViewInterceptor 빈을 생성한다.

**/
    @Bean
    public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
       if (this.jpaProperties.getOpenInView() == null) {
          logger.warn("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");
       }
       return new OpenEntityManagerInViewInterceptor();
    }

/**

* openEntityManagerInViewInterceptor 빈을 WebRequestInterceptor에 추가한다.

**/
    @Bean
    public WebMvcConfigurer openEntityManagerInViewInterceptorConfigurer(
          OpenEntityManagerInViewInterceptor interceptor) {
       return new WebMvcConfigurer() {

          @Override
          public void addInterceptors(InterceptorRegistry registry) {
             registry.addWebRequestInterceptor(interceptor);
          }
...후략


OpenEntityManagerInViewInterceptor는 Web Request Interceptor 중 하나다. 웹으로부터 요청이 들어오면 요청을 가로채서 무언가를 한다. 그 무엇인가가 무엇인고 하면 OSIV의 시작과 끝맺음이다. 자세한 내용은 코드를 보며 살펴보자.


public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAccessor implements AsyncWebRequestInterceptor {



OpenEntityManagerInViewInterceptor와 OSIV의 시작


시작은 매우 단순한다. 요청이 들어오면 인터셉터의 prehandle() 메서드가 호출돼 요청을 받는다.

TransactionSynchronizationManager를 통해 현재 요청을 처리하는 스레드에 EntityManager를 생성해서 추가하는 것이다. 이게 다라고? 맞다. 이게 전부이다.


@Override
public void preHandle(WebRequest request) throws DataAccessException {
    (중략)...

   /**

    * EntityManager를 가져온다.

    **/
    EntityManagerFactory emf = obtainEntityManagerFactory();
 ...(중략)
    else {

    
       logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor");
       try {

        /**

        * 1. EntityManager를 생성한다.

          *  2. EntityManager를 현재 Thread에 바인딩한다. (TransactionSynchronizationManager 활용)

        **/
          EntityManager em = createEntityManager();
          EntityManagerHolder emHolder = new EntityManagerHolder(em);
          TransactionSynchronizationManager.bindResource(emf, emHolder);

... (후략)


간단한 추가 동작이지만 TransactionSynchronizationManager를 통해 스레드에 EntityManager가 바인딩되면 추후에 EntityManager의 라이프 사이클이 완전히 달라진다.



트랜잭션의 시작과 EntityManager의 생성


트랜잭션이 시작될 때 생성되고 종료될 때 소멸하는 게 EntityManager의 기본적인 라이프사이클이다. 트랜잭션과 엔티티매니저는 생명주기를 같이한다.


아래 코드는 트랜잭션이 시작될 때 호출되는 JpaTransactionManager의 doGetTransaction 메서드다.

(코드)

@Override
protected Object doGetTransaction() {
    JpaTransactionObject txObject = new JpaTransactionObject();
    txObject.setSavepointAllowed(isNestedTransactionAllowed());

/**

* 현재 스레드에 바인딩된 EntityManager를 조회한다.

* EntityManager가 존재하면(!=null)  트랜잭션 객채(txObject)에 EntityManager를 바인딩한다.

**/

    EntityManagerHolder emHolder = (EntityManagerHolder)
          TransactionSynchronizationManager.getResource(obtainEntityManagerFactory());
    if (emHolder != null) {
       if (logger.isDebugEnabled()) {
          logger.debug("Found thread-bound EntityManager [" + emHolder.getEntityManager() +
                "] for JPA transaction");
       }
       txObject.setEntityManagerHolder(emHolder, false);
    }
...(중략)
    return txObject;
}


TransactionSynchronizationManager에 추가된 EntityManager를 가져와 트랜잭션 객체에 추가한다. setEntityManagerHolder 호출을 통해서 트랜잭션 객체에 EntityManager가 바인딩된다.


setEntityManagerHolder의 두 번째 파라미터 newEntityManagerHolder를 눈여겨봐야 한다. false로 값을 세팅해주고 있다. 바인딩된 EntityManager가 신규 생성 됐는지 여부를 알려주는 Flag이다. 트랜잭션과 함께 생성된 EntityManager라면 true가 되고 아니라면 값은 false가 될 것이다.

public void setEntityManagerHolder(

    @Nullable EntityManagerHolder entityManagerHolder,

    boolean newEntityManagerHolder // 두 번째 파라미터

) {
    this.entityManagerHolder = entityManagerHolder;
    this.newEntityManagerHolder = newEntityManagerHolder;
}


OSIV 하에서는 EntityManager가 TransactionSynchronizationManager에 기존에 존재하고 있었기 때문에 false가 된다. newEntityManagerHolder도 EntityManager의 라이프사이클을 결정하는 중요한 값이다. 뒤에서 어떻게 활용될 예정이므로 기억해 두자.

미리 설명해 두면 newEntityManagerHolder는 트랜잭션이 커밋 후 JpaTransactionManager가 EntityManager를 닫을지 말지를 결정할 때 참조하는 필드이다.


보너스

다음으로 넘어가기 전에 보너스로 OSIV가 활성화 돼있지 않다면 EntityManager는 어떻게 트랜잭션 객체에 추가되는 지 알아보자.


이후에 doBegin 메서드에서 신규로 생성된다. 그리고 두 번째 파라미터 newEntityManagerHoldertrue가 된다.

OpenEntityManagerInViewInterceptor에서 EntityManager가 바인딩된 경우를 제외하곤 대부분 신규로 생성된다고 보면 된다.

(코드)

@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
    JpaTransactionObject txObject = (JpaTransactionObject) transaction;
...(중략)

    try {

/**

* 트랜잭션 객체에 EntityManager가 바인딩 안 됐다면 신규로 생성해 추가한다.

**/
       if (!txObject.hasEntityManagerHolder() ||
             txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
          EntityManager newEm = createEntityManagerForTransaction();
          if (logger.isDebugEnabled()) {
             logger.debug("Opened new EntityManager [" + newEm + "] for JPA transaction");
          }
          txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true);
       }




EntityManager 죽느냐 사느냐. 그것은 newEntityManagerHolder의 문제이다


JpaTransactionManager는 트랜잭션이 종료되면 doCleanupAfterCompletion을 호출한다.

트랜잭션과 EntityManager는 생명주기를 함께 했다고 앞서 말했다. 따라서 트랜잭션 소멸 메서드인 doCleanupAfterCompletion에서 EntityManager도 소멸하게 된다.

@Override
protected void doCleanupAfterCompletion(Object transaction) {
    JpaTransactionObject txObject = (JpaTransactionObject) transaction;
...(중략)
    

 
    // Remove the entity manager holder from the thread.
    if (txObject.isNewEntityManagerHolder()) { // 트랜잭션 객체의 EntityManager가 신규 생성됐는가?
       EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
       if (logger.isDebugEnabled()) {
          logger.debug("Closing JPA EntityManager [" + em + "] after transaction");
       }

     // EntityManager를 소멸한다.
       EntityManagerFactoryUtils.closeEntityManager(em);
    }
    else {
       logger.debug("Not closing pre-bound JPA EntityManager after transaction");
    }
}


EntityManagerFactoryUtils.closeEntityManager(em)를 호출해 EntityManager를 소멸한다. 하지만 EntityManager의 소멸은 조건부이다.  if (txObject.isNewEntityManagerHolder()) 조건이 참일 경우에만 EntityManager를 소멸한다.


isNewEntityManagerHolder 메서드는 newEntityManagerHolder의 값을 그대로 반환한다. OSIV가 활성화된 경우에는 항상 if (txObject.isNewEntityManagerHolder())조건을 충족할 수 없으므로 EntityManager는 트랜잭션과 함께 소멸되지 않는다.


OSIV 하에서 트랜잭션이 종료된 후에도 엔티티 지연 로딩이 가능한 이유이다.


스레드에 EntityManager가 상주하고 있는 거랑 지연로딩이랑 무슨 상관이죠? 이건 다음 글에서 자세하게 다시 알아보자.


그러면 EntityManager는 언제 닫힐까?


앞서서 OpenEntityManagerInViewInterceptor가 OSIV의 시작과 끝맺음을 한다고 했던 것을 기억하는가? EntityManager를 시작부에서 생성했듯이 소멸의 역할도 하며 끝맺음을 한다.

그 시점은 request가 종료되고 OpenEntityManagerInViewInterceptor의 afterCompletion이 호출될 때이다.

@Override
public void afterCompletion(WebRequest request, @Nullable Exception ex)  {
    if (!decrementParticipateCount(request)) {

/**

* 스레드에서 EntityMananger를 해제하고 소멸한다.

**/
       EntityManagerHolder emHolder = (EntityManagerHolder)
             TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
       logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewInterceptor");
       EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
    }
}


요약하면

1. open-in-view: true로 설정하면 OpenEntityManagerInViewInterceptor가 요청을 가로챈다.

2. 요청이 시작되면 인터셉터에서 EntityManager를 생성한다.

3. (트랜잭션이 종료될 때가 아니라) 응답이 나갈 때 인터셉터에서 EntityManager를 소멸한다.

4. 즉 요청을 처리하는 동안 EntityManager는 스레드에 상주하며 마르고 닳도록 재사용된다.


이것이 간단하지만 복잡한 OSIV의 구현이다.




EntityManager의 재사용 OSIV가 효율적인 방안인가?


EntityManager를 여러 차례 생성하지 않고 재사용한다고 하면 마냥 효율적인 방안이라고 생각할 수 있다.

하지만 EntityManage가 열려있다는 것은 Connection도 열려있는 상태를 유지한다는 의미이기도 하다. EntityManager는 Connection 객체를 한번 연결하면 종료하지 않고 다음 요청에 재사용한다.


LogincalConnectionManagedImpl.java

private Connection acquireConnectionIfNeeded() {

    if ( physicalConnection == null ) {

    /**

    * 기존에 생성해 둔 연결이 없을 경우에만 신규로 생성한다.

    **/
       try {
          physicalConnection = jdbcConnectionAccess.obtainConnection();
       }
       catch (SQLException e) {
          throw sqlExceptionHelper.convert( e, "Unable to acquire JDBC Connection" );
       }
       finally {
          observer.jdbcConnectionAcquisitionEnd( physicalConnection );
       }
    }
    return physicalConnection;
}


즉 트랜잭션이 종료된 후에도 데이터베이스 연결을 점유하고 있다는 의미가 된다. 트랜잭션이 종료된 후에 요청이 짧은 시간 안에 완료된다면 큰 문제가 안되지만 트랜잭션 밖의(데이터베이스 작업과 상관없는) 별개의 큰 작업이 남아있다면 오히려 비효율적인 결과를 초래할 수도 있으므로 주의해야 한다.


진짜 끝

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari