[Spring Data JPA] 커넥션은 언제 릴리즈 될까 (w/ @Transactional)
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() 도 이어서 트레이싱할 수 있긴하나 여기서 마친다.