📚 개발백과

[Spring Data JPA] 커넥션은 언제 릴리즈 될까 (w/ @Transactional)

the0 2025. 6. 26. 16:53
728x90

Spring Data JPA 와 Hibernate를 사용하고

@Transactional 어노테이션이 명시된 서비스 레이어의 메서드가 존재한다고 가정한다.

 

이 아티클은 connection.close()가 실행되는 step을 트레이싱한다.

 


 

AbstractPlatformTransactionManager에서는

doCommit() 혹은 doRollback() 이후 doCleanupAfterCompletion()이 실행된다.

 

레퍼런스

 

 

Spring Data JPA 를 사용하고 있으므로,

 

현재 사용되는 트랜잭션 매니저는 PlatformTransactionManager의 구현체 중 하나인 JpaTransactionManager이다.

 

JpaTransactionManager.java의 doCleanupAfterCompletion() 코드는 다음과 같다.

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

    // Remove the entity manager holder from the thread, if still there.
    // (Could have been removed by EntityManagerFactoryUtils in order
    // to replace it with an unsynchronized EntityManager).
    if (txObject.isNewEntityManagerHolder()) {
        TransactionSynchronizationManager.unbindResourceIfPossible(obtainEntityManagerFactory());
    }
    txObject.getEntityManagerHolder().clear();

    // Remove the JDBC connection holder from the thread, if exposed.
    if (getDataSource() != null && txObject.hasConnectionHolder()) {
        TransactionSynchronizationManager.unbindResource(getDataSource());
        ConnectionHandle conHandle = txObject.getConnectionHolder().getConnectionHandle();
        if (conHandle != null) {
            try {
                getJpaDialect().releaseJdbcConnection(conHandle,
                        txObject.getEntityManagerHolder().getEntityManager());
            }
            catch (Throwable ex) {
                // Just log it, to keep a transaction-related exception.
                logger.error("Failed to release JDBC connection after transaction", ex);
            }
        }
    }

    getJpaDialect().cleanupTransaction(txObject.getTransactionData());

    // Remove the entity manager holder from the thread.
    if (txObject.isNewEntityManagerHolder()) {
        EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
        if (logger.isDebugEnabled()) {
            logger.debug("Closing JPA EntityManager [" + em + "] after transaction");
        }
        EntityManagerFactoryUtils.closeEntityManager(em); //❗️❗️❗️
    }
    else {
        logger.debug("Not closing pre-bound JPA EntityManager after transaction");
    }
}

 

 

위 메서드에서 DB connection 릴리즈와 관련이 있는 코드는 다음과 같다.

// 마지막 if문 내부에 있음
EntityManagerFactoryUtils.closeEntityManager(em);

`


 

EntityManagerFactoryUtils.java의 closeEntityManager() 코드는 다음과 같다.

/**
 * Close the given JPA EntityManager,
 * catching and logging any cleanup exceptions thrown.
 * @param em the JPA EntityManager to close (may be {@code null})
 * @see jakarta.persistence.EntityManager#close()
 */
public static void closeEntityManager(@Nullable EntityManager em) {
    if (em != null) {
        try {
            if (em.isOpen()) {
                em.close(); //❗️❗️❗️
            }
        }
        catch (Throwable ex) {
            logger.error("Failed to release JPA EntityManager", ex);
        }
    }
}

 

 

위 메서드에서 DB connection 릴리즈와 관련이 있는 코드는 다음과 같다.

em.close();

 


Spring Data JPA + Hibernate 를 사용하고 있으므로, 

 

Hibernate가 JPA의 EntityManager 인터페이스를 Session으로 구현하고,

 

Session은 다시 내부적으로 SessionImpl로 구현되고 있다.

 

SessionImpl.java 의 close() 코드는 다음과 같다.

closeWithoutOpenChecks() 도 함께 가져왔다.

 

@Override
public void close() throws HibernateException {
    if ( isClosed() ) {
        if ( getFactory().getSessionFactoryOptions().getJpaCompliance().isJpaClosedComplianceEnabled() ) {
            throw new IllegalStateException( "Illegal call to #close() on already closed Session/EntityManager" );
        }

        log.trace( "Already closed" );
        return;
    }

    closeWithoutOpenChecks();
}

public void closeWithoutOpenChecks() throws HibernateException {
    if ( log.isTraceEnabled() ) {
        log.tracef( "Closing session [%s]", getSessionIdentifier() );
    }

    final EventManager eventManager = getEventManager();
    final HibernateMonitoringEvent sessionClosedEvent = eventManager.beginSessionClosedEvent();

    // todo : we want this check if usage is JPA, but not native Hibernate usage
    final SessionFactoryImplementor sessionFactory = getSessionFactory();
    try {
        if ( sessionFactory.getSessionFactoryOptions().isJpaBootstrap() ) {
            // Original hibernate-entitymanager EM#close behavior
            checkSessionFactoryOpen();
            checkOpenOrWaitingForAutoClose();
            if ( fastSessionServices.discardOnClose || !isTransactionInProgressAndNotMarkedForRollback() ) {
                super.close(); //❗️❗️❗️
            }
            else {
                //Otherwise, session auto-close will be enabled by shouldAutoCloseSession().
                prepareForAutoClose(); //❗️❗️❗️
            }
        }
        else {
            super.close(); //❗️❗️❗️
        }
    }
    finally {
        final StatisticsImplementor statistics = sessionFactory.getStatistics();
        if ( statistics.isStatisticsEnabled() ) {
            statistics.closeSession();
        }

        eventManager.completeSessionClosedEvent( sessionClosedEvent, this );
    }
}

 

 

위 메서드에서 DB connection 릴리즈와 관련있는 코드는 다음과 같다.

super.close();
// 혹은
prepareForAutoClose();

 

 

super.close() 명령이 실행되는 조건은 두 가지 경우가 있다.

 

1. sessionFactory.getSessionFactoryOptions().isJpaBootstrap() 이 false 인 경우

즉, JPA 부트스트랩 모드가 아닌 경우이다.

SessionFactoryOptions.java#isJpaBootstrap() 주석은 다음과 같다.

Was building of the SessionFactory initiated through JPA bootstrapping, or through Hibernate's native bootstrapping?
Returns:true indicates the SessionFactory was built through JPA bootstrapping; 
false indicates it was built through native bootstrapping.

해석-
SessionFactory가 JPA 부트스트래핑으로 빌드되었으면 true,
HIbernate의 네이티브 부트스트래핑으로 빌드외었으면 false.

결론-
Spring Data JPA를 사용하면 isJpaBootstrap() == true이고, Hibernate만 직접 사용하면 false이다.

 

 

2. Hibernate가 JPA 부트스트랩 모드이면서 2-1 혹은 2-2 둘 중 하나만 만족해도 세션이 정리된다. (DB 커넥션 릴리즈)

 

2-1. fastSessionServices.discardOnClose == true -> 즉시 세션 정리

2-2. !isTransactionInProgressAndNotMarkedForRollback() == true

트랜잭션이 진행 중이지 않거나 rollback된 상태 둘 중 하나라도 참이면 된다.

내부 코드는 다음과 같다.
private boolean isTransactionInProgressAndNotMarkedForRollback() {
    if ( waitingForAutoClose ) {
        return getSessionFactory().isOpen()
            && getTransactionCoordinator().isTransactionActive( false );
    }
    else {
        return !isClosed()
            && getTransactionCoordinator().isTransactionActive( false );
    }
}​

 


 

prepareForAutoClose() 명령이 실행되는 조건은 다음과 같다.

 

Hibernate가 JPA 부트스트랩 모드이면서 2-1 혹은 2-2 둘 다 만족하지 않는다.

 

2-1. fastSessionServices.discardOnClose == false

2-2. !isTransactionInProgressAndNotMarkedForRollback() == false

즉, isTransactionInProgressAndNotMarkedForRollback() == true.

트랜잭션이 진행 중이면서 아직 rollback 된 상태로 마킹되지 않음

 

 

prepareForAutoClose() 의 내부 코드는 AbstractSharedSessionContract.java에서 확인할 수 있다.

protected void prepareForAutoClose() {
    waitingForAutoClose = true;
    closed = true;
    // For non-shared transaction coordinators, we have to add the observer
    if ( !isTransactionCoordinatorShared ) {
        addSharedSessionTransactionObserver( transactionCoordinator );
    }
}

protected void addSharedSessionTransactionObserver(TransactionCoordinator transactionCoordinator) {
}

 

 

다만 addSharedSessionTransactionObserver는 해당 클래스에 구현되어있지 않고,

 

SessionImpl 에서 구현되어 있다.

구현체는 다음과 같다.

@Override
protected void addSharedSessionTransactionObserver(TransactionCoordinator transactionCoordinator) {
    transactionObserver = new TransactionObserver() {
        @Override
        public void afterBegin() {
        }

        @Override
        public void beforeCompletion() {
            if ( isOpen() && getHibernateFlushMode() !=  FlushMode.MANUAL ) {
                managedFlush();
            }
            actionQueue.beforeTransactionCompletion();
            beforeTransactionCompletionEvents();
        }

        @Override
        public void afterCompletion(boolean successful, boolean delayed) {
            afterTransactionCompletion( successful, delayed );
            if ( !isClosed() && autoClose ) {
                managedClose(); //❗️❗️❗️
            }
        }
    };
    transactionCoordinator.addObserver( transactionObserver );
}

 

 

위의 메서드에서 DB 커넥션 릴리즈와 관련있는 코드는 다음과 같다.

managedClose()

 

 

managedClose() 또한 SessionImpl 에 구현되어 있다.

private void managedClose() {
    log.trace( "Automatically closing session" );
    closeWithoutOpenChecks();
}

 

 

mangedClose()에서 호출하는 closeWithoutOpenChecks()는

SessionImpl#close() 내부에서 세션을 닫기 위해 호출하는 메서드 closeWithoutOpenChecks()와 동일하다.

 

즉, closeWithoutOpenChecks() -> prepareAutoClose() -> ... -> closeWithoutOpenChecks() 로 다시 돌아오게 된다.

 

그리고 다시 돌아왔을 때는 `!isTransactionInProgressAndNotMarkedForRollback() == true` 이므로 super.close()가 실행된다.


 

어찌되었든 호출된 super.close() 를 다시 파고든다.

 

SessionImpl.java#closeWithoutOpenChecks() 의 super.close()는 AbstractSharedSessionContract.java에 구현되어 있다.

 

@Override
public void close() {
    if ( closed && !waitingForAutoClose ) {
       return;
    }

    try {
       delayedAfterCompletion();
    }
    catch ( HibernateException e ) {
       if ( getFactory().getSessionFactoryOptions().isJpaBootstrap() ) {
          throw getExceptionConverter().convert( e );
       }
       else {
          throw e;
       }
    }

    if ( sessionEventsManager != null ) {
       sessionEventsManager.end();
    }

    if ( transactionCoordinator != null ) {
       removeSharedSessionTransactionObserver( transactionCoordinator );
    }

    try {
       if ( shouldCloseJdbcCoordinatorOnClose( isTransactionCoordinatorShared ) ) {
          jdbcCoordinator.close(); // ✅
       }
    }
    finally {
       setClosed();
    }
}

 

위의 메서드에서 DB 커넥션 릴리즈와 관련있는 코드는 다음과 같다.

jdbcCoordinator.close();

 


 

jdbcCoordinator는 인터페이스로, 이를 구현한 구현체는 JdbcCoordinatorImpl.java이다.

 

JdbcCoordinatorImpl.java의 close() 메서드는 다음과 같다.

@Override
public Connection close() {
    LOG.tracev( "Closing JDBC container [{0}]", this );
    Connection connection;
    try {
       if ( currentBatch != null ) {
          LOG.closingUnreleasedBatch();
          currentBatch.release();
       }
    }
    finally {
       connection = logicalConnection.close(); // ✅
    }
    return connection;
}

 

위의 메서드에서 DB 커넥션 릴리즈와 관련있는 코드는 다음과 같다.

connection = logicalConnection.close();

 

LogicalConnection는 인터페이스이다.

 

LogicalConnection

|

LogicalConnectionImplementor

|

AbstractLogicalConnectionImplementor
|

LogicalConnectionManagedImpl

 

LogicalConnectionManagedImpl에서 구현된 close() 는 다음과 같다.

 

@Override
public Connection close() {
    if ( closed ) {
       return null;
    }

    getResourceRegistry().releaseResources();

    log.trace( "Closing logical connection" );
    try {
       releaseConnection(); // ✅
    }
    finally {
       // no matter what
       closed = true;
       log.trace( "Logical connection closed" );
    }
    return null;
}

 

 

위의 메서드에서 DB 커넥션 릴리즈와 관련있는 코드는 다음과 같다.

releaseConnection()

 

해당 메서드는 동일한 클래스 내에 다음과 같이 구현되어 있다.

 

private void releaseConnection() {
    final Connection localVariableConnection = this.physicalConnection;
    if ( localVariableConnection == null ) {
       return;
    }

    // We need to set the connection to null before we release resources,
    // in order to prevent recursion into this method.
    // Recursion can happen when we release resources and when batch statements are in progress:
    // when releasing resources, we'll abort the batch statement,
    // which will trigger "logicalConnection.afterStatement()",
    // which in some configurations will release the connection.
    this.physicalConnection = null;
    try {
       try {
          getResourceRegistry().releaseResources();
          if ( !localVariableConnection.isClosed() ) {
             sqlExceptionHelper.logAndClearWarnings( localVariableConnection );
          }
       }
       finally {
          jdbcConnectionAccess.releaseConnection( localVariableConnection ); // ✅
       }
    }
    catch (SQLException e) {
       throw sqlExceptionHelper.convert( e, "Unable to release JDBC Connection" );
    }
    finally {
       observer.jdbcConnectionReleaseEnd();
    }
}

 

위의 메서드에서 DB 커넥션 릴리즈와 관련있는 코드는 다음과 같다.

jdbcConnectionAccess.releaseConnection( localVariableConnection );

 


 

jdbcConnectionAccess는 인터페이스로, 이를 구현한 구현체는 JdbcConnectionAccessConnectionProviderImpl이다.

 

JdbcConnectionAccessConnectionProviderImpl.java 의 releaseConnection() 은 다음과 같다.

 

@Override
public void releaseConnection(Connection connection) throws SQLException {
    if ( connection != this.jdbcConnection ) {
       throw new PersistenceException(
             String.format(
                   "Connection [%s] passed back to %s was not the one obtained [%s] from it",
                   connection,
                   JdbcConnectionAccessConnectionProviderImpl.class.getName(),
                   jdbcConnection
             )
       );
    }

    // Reset auto-commit
    if ( !wasInitiallyAutoCommit ) {
       try {
          if ( jdbcConnection.getAutoCommit() ) {
             jdbcConnection.setAutoCommit( false );
          }
       }
       catch (SQLException e) {
          log.info( "Was unable to reset JDBC connection to no longer be in auto-commit mode" );
       }
    }

    // Release the connection
    connectionProvider.closeConnection( jdbcConnection ); // ✅
}


위의 메서드에서 DB 커넥션 릴리즈와 관련있는 코드는 다음과 같다.

connectionProvider.closeConnection( jdbcConnection );

 


 

현재 Hikari CP를 사용하고 있으므로, 이를 기준으로 설명을 한다.

 

ConnectionProvider는 인터페이스로, 이를 구현한 구현체는 HikariConnectionProvider.java에 있다.

 

HikariConnectionProvider.java의 closeConnection() 은 다음과 같다.

 

@Override
public void closeConnection(Connection conn) throws SQLException
{
   conn.close();
}

 

 

conn.close() 도 이어서 트레이싱할 수 있긴하나 여기서 마친다.

 

 

728x90