스프링 프레임워크에서는 트랜잭션 처리를 쓰레드에 동기화하고 추상화하기 위한 인터페이스를 제공한다.
PlatformTransactionManager
는 내부적으로 트랜잭션 동기화 매니저를 이용해 쓰레드에 커넥션을 동기화하고, 이 동기화된 커넥션을 활용해서 트랜잭션을 처리한다. 덕분에 비즈니스 로직에서는 직접 커넥션을 이용해 커밋, 롤백을 할 필요 없이 추상화된 트랜잭션 매니저 인터페이스에 의존하여 트랜잭션을 수행할 수 있다.
트랜잭션 동기화를 담당하는 컴포넌트는 다음과 같다. org.springframework.transaction.support.TransactionSynchronizationManager
리포지토리 컴포넌트에서는 동기화된 커넥션을 획득하기 위해 DataSourceUtils
를 이용한다. DataSourceUtils
는 특정 데이터소스에 대해 쓰레드에 동기화된 커넥션이 있을 경우 해당 커넥션을 반환하고, 없다면 새로 생성하여 반환한다. 쓰레드에 커넥션을 동기화하기 위해 내부적으로 ThreadLocal
을 사용한다.
커넥션 획득과 반환
// 커넥션 획득
Connection con = DataSourceUtils.getConnection(dataSource);
// 커넥션 반환
DataSourceUtils.releaseConnection(con, dataSource);
트랜잭션 수행
//트랜잭션 메니저 생성
PlatformTransactionManager transactionManager = new PlatformTransactionManager(dataSource);
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
//비즈니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
하지만 트랜잭션 매니저를 이용하더라도 여전히 중복해서 사용해야 하는 코드가 너무 많다. 트랜잭션을 시작하고, 커밋하고, 롤백하는 코드를 작성할 필요 없이 비즈니스 로직만 작성할 수 있다면 매우 편할 것이다.
이것을 위해 스프링에서는 @Transactional
어노테이션을 제공한다. 비즈니스 로직 메소드에 이 어노테이션을 붙이기만 하면 [[Spring AOP]]에서 자동으로 트랜잭션을 수행하는 프록시를 만들어 트랜잭션을 수행해준다.
// 트랜잭션이 알아서 수행된다.
@Transactional
public void bizLogic() {
...logic
}
@Transactional
어노테이션은 메소드 뿐만 아니라 클래스에도 추가할 수 있다. 이 경우 해당 클래스의 모든 퍼블릭 메소드들에 트랜잭션이 적용된다.
@Transactional
public class Repository {
public save(); // transaction applied
private saveInner(); // transaction not applied
}
트랜잭션 수행을 위해 트랜잭션 매니저가 스프링 컨테이너에 빈으로 등록되어 있어야 한다. 하지만 스프링 부트에서는 기본적으로 트랜잭션 매니저 및 데이터소스를 자동으로 등록해주므로 크게 신경쓸 일은 없다.
데이터소스는 HikariCP
를 이용한다. 데이터소스 설정은 application.properties
에 다음과 같이 설정을 추가해주면 된다.
application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
선언적 트랜잭션과 내부 트랜잭션 호출 문제
Spring AOP
를 이용해 트래잭션 프록시를 사용하는 경우에 트랜잭션 메소드를 내부적으로 호출하게 되면 프록시가 적용되지 않은 원래의 메소드가 호출되어 트랜잭션이 적용되지 않는다.
이런 경우는 내부적으로 사용하는 메소드들을 따로 분리하여 사용함으로써 문제를 해결할 수 있다.
트랜잭션 전파
하나의 트랜잭션이 진행중인데 또다른 트랜잭션이 새로 시작된다면 새로운 트랜잭션은 원래의 트랜잭션에 참여하게 된다. 따라서 새로운 트랜잭션이 생성되지 않는다. 이 경우에는 두 트랜잭션이 모두 커밋해야 전체가 커밋된다.
만약 내부 트랜잭션이 롤백되었는데 외부 트랜잭션이 커밋을 시도한다면 UnexpectedRollbackException
예외가 발생하며 트랜잭션이 롤백된다.
만약 기존의 트랜잭션이 존재할 때 참여하지 않고 새 트랜잭션을 만들도록 하고 싶다면 전파 속성을 REQUIRES_NEW
로 설정해주면 된다.