ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Data JPA] 커넥션은 언제 릴리즈 될까 (w/ @Transactional)
    📚 개발백과 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
Designed by Tistory.