-
[Spring Data JPA] 커넥션은 언제 릴리즈 될까 (w/ @Transactional)📚 개발백과 2025. 6. 26. 16:53728x90
Spring Data JPA 와 Hibernate를 사용하고
@Transactional 어노테이션이 명시된 서비스 레이어의 메서드가 존재한다고 가정한다.
이 아티클은 connection.close()가 실행되는 step을 트레이싱한다.
AbstractPlatformTransactionManager에서는
doCommit() 혹은 doRollback() 이후 doCleanupAfterCompletion()이 실행된다.
레퍼런스
더보기0. AbstractPlatformTransactionManager 의 doCleanupAfterCompletion() 설명
1. 시퀀스 다이어그램 참고
https://dhsim86.github.io/web/2017/11/04/spring_custom_transactionmanager-post.html
Dongho Sim's dev story|스프링 Custom TransactionManager 구현
Stats: comments
dhsim86.github.io
2. 인프런 답변 참고
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'📚 개발백과' 카테고리의 다른 글
[Fixture Monkey] 불변 객체를 위한 사용자 정의 인트로스펙터 예제 코드 - 기초편 (0) 2025.10.06 [Spring Data JPA] 서비스 레이어의 어느 메서드에 @Transactional을 안붙이면? (2) 2025.06.27 [FastAPI, PostgreSQL] postgresql://와 postgresql+asyncpg:// 의 차이 (1) 2024.10.18 오프라인 상태에서 cURL 사용하기 (0) 2024.10.01 [Docker] 컨테이너 실행 시 외부에서 .yml 주입하기 (1) 2024.09.26