상세 컨텐츠

본문 제목

Spring Transaction 사용 시 주의할 점

Spring Data

by Wanderer Kim 2021. 9. 25. 20:39

본문

728x90

트랜잭션 안에서 트랜잭션을 새로 여는 경우

PROPAGATION_REQUIRED를 사용할 때 주의 사항

Spring이 기본값으로 사용하는 propagation behavior는 PROPAGATION_REQUIRED 이다. 이 옵션을 사용하면 어떤 트랜잭션 안에서 TransactionTemplate 을 통해 트랜잭션을 열려고 시도할 경우, AbstractPlatformTransactionManager.getTransact 는 이미 열려있는 기존 트랜잭션을 반환 한다.

 

이로 인해 발생하는 눈여겨 볼만한 특징에는 두 가지가 있다. 첫 번째 포인트는 안쪽 트랜잭션이 롤백되면 바깥쪽 트랜잭션도 롤백된다는 것이다. 이는 코드 상으로 분리되어 보이는 두 트랜잭셔닝 사실 한 트랜잭션 안에서 실행되고 있기 때문이다.

transactionTemplate.execute{
	val person = Person("test");
    personRepository.save(person);
    
    try{
    	transactionTemplate.execute{
        	throw new RuntimeException("some unexpeced exception");
        }
    } catch(Exception e){
    }
}

위 코드가 실행되더라고 Person("test")는 DB에 저장되지 않는다. 안쪽 트랜잭션에서 예외가 던져지면 해당 쓰레드에 rollback only mark가 남는다. 그리고 바깥쪽 트랜잭션이 커밋되려고 하면 이 rollback only mark 때문에 UnexpectedRollbackException 예외가 던져지면서 트랜잭션이 커밋되지 앟고 롤백된다.

 

두 번째는 내부 트랜잭션을 열 때 사용한 TransactionDefinition 이 적용되지 않는다는 점이다. 

val seriallizableTxTemplate = TransactionTemplate().apply{
	transactionManager = transactonTemplate.transactionManager
    isolationLevel = TransactionDefinition.ISOLATION_SERIALIZABLE
    propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED
}

transactionTemplate.execute{
	println("hihi 1");
    
    serializableTxTemplate.execute{
    	println("hihi 2");
    }
}

위 코드의 경우 serializableTxTemplate.execute{ } 는 기대한 대로 새로운 physical connection에서 새로운 entity manager를 가지고 isolation level이 SERIALIZABLE인 새로운 트랜잭션을 연다. 두 트랜잭션은 이제 롤백도 독립적으로 이루어진다.(물론 안쪽 트랜잭션에서 예외가 던져졌을 때는 바깥쪽 트랜잭션에서 try-catch로 감싸야 바깥쪽 트랜잭션이 롤백되지 않는다.)

 

하지만 완전히 새로운 트랝개션이 열리기 때문에 주의해야 할 점도 생긴다. 일단 connection pool의 connection을 한 개 더 차지한다. 또한, 독립적으로 열린 두 트랜잭션 사이에서 데드락이 걸릴 수 있다. 두 트랜잭션은 entity manager를 공유하지 않기 때문에 persistence context역시 공유하지 않고, 이로 인한 쿼리 실행의 비효율이 발생할 수 있다.

 

TransactionSynchronization.afterCommit()을 사용하는 경우

새로운 트랜잭션을 열 때 주의사항

트랜잭션 Synchronization 중 afterCommit() 관련 코드는 AbstractPlatformTransactionManager.processCommit 에서 찾아볼 수 있다. 함수의 흐름을 대강 이야기하면 아래와 같다.

 

  1. 실제 commit을 수행한다. (AbstractPlatformTranscionManager.doCommit())
  2. after commit을 수행한다. (TransactionSynchronization.afterCommit())
  3. after completion을 수행한다. (TransactionSynchronization.afterCompletion())
  4. 트랜잭션 리소스를 정리한다. (AbstractPlatformTransactionManager.cleanupAfterCompletion()

여기서 중요한 사실은, 4번에서 트랜잭션 리소스가 정리되기 전까지 기존 트랜잭션에서 사용한 여러가지 리소스, 즉 TransactionDefinition, entity manager, physical connection을 여전히 살아있는 상태다. 그래서 afterCommit() 안에서 트랜잭션을 열면 기존의 physical connection 위에서, 기존의 entity manager를 가지고, 기존의 TransactionDefinition를 사용해서 트랜잭션이 열린다. 그래서 트랜잭션 안에서 트랜잭션을 열려고 하는 상황과 동일하게 TransactionDefinition 이 제대로 동작하지 않는다. 이 문제는 마찬가지로 PROPAGATION_REQUIRES_NEW 를 사용하면 해결할 수 있다.

 

afterCommit() 안에서 트랜잭션을 여는 것이 트랜잭션 안에서 트랜잭션을 새로 여는 경우와 다른점은, 이미 기존 트랜잭션이 커밋되었다는 사실이다. 즉, afterCommit() 안에서 새로운 트랜잭션을 열려고 하면 실제로 DB에서 새로운 트랜잭션이 열린다. 따라서 afterCommit() 안에서의 트랜잭션이 롤백되더라도 기존 트랜잭션은 롤백되지 않는다.

 

JPA를 사용할 때 주의사항

이번에는 JPA를 사용할때 기존 트랜잭션에서 가져온 entity를 afterCommit() 안에서 접근해서 lazy load 하려고 하면 어떻게 동작하는지 알아보려고 한다.

 

transactionTemplate.execute{
	val person = personRepository.findFirstByName("Test");
    
    TransactionSynchronizationManager.registerSynchronization(Object : TransactionSynchronization{
    	override fun afterCommit(){
        	println(person.home.addresss)
        }
    })
}

위 코드 동작 결과는 lazy load가 잘 된다. 이유는 entity를 lazy load 할 수 없게 되는 시점에 cleanupAfterCompletion() 이기 때문이다. cleanupAfterCompletion() 에서 JpaTransactionManager는 entity manager와 persistence context를 닫고, persistence context의 entity를 detached 상태로 만든다. 이 때 entity는 lazy loading을 할 수 없는 상태로 빠진다. 따라서 cleanupAftetCompletion() 이전에 호출되는 afterCommit() 내부에서는 기존 트랜잭션에서 불러온 entity에 안전하게 접근하고 lazy load 할 수 있다.

반응형

'Spring Data' 카테고리의 다른 글

트랜잭션 이해  (0) 2025.01.05
DataSource 이해  (0) 2024.11.30
커넥션 풀  (0) 2024.11.17
선언적 Transactional  (0) 2021.09.19

관련글 더보기

댓글 영역