Spring transaction
1. 트랜잭션(Transaction) 이란?
더 이상 쪼갤 수 없는 논리적인 작업단위 이다. 가장 많이 예를 드는 것으로 송금 예제가 있다. A가 B에게 5000원을 송금하려고 할 때 비즈니스적으로는 두 단계가 있다.
- A의 계좌에서 5000원 인출
- B의 계좌에 5000원 입금
이 두 단계가 동시에 실행되거나 동시에 실행되지 않아야 한다. 만약 1만 실행되면 A는 5000원이 없어졌는데 그 돈은 사라져 버려 심각한 논리적인 오류가 발생한다.
따라서 이렇게 1, 2 작업을 하나의 트랜잭션 단위로 묶어야 한다.
스프링은 선언적 트랜잭션으로 아래 코드와 같이 원하는 곳에 @Transactional 어노테이션만 붙이면 트랜잭션이 가능하게 된다. (class 위에 @Transactional 을 사용하면 안에 있는 모든 메서드들마다 각각 트랜잭션 적용이 된다.)
@Service
public class RemitService {
private RemitRepository remitRepository;
// 여기의 모든 메서드는 Transactional 이 작용된다.
@Transactional
public void remit(Member from, Member to, int money) {
// from의 계좌에서 돈을 money만큼 인출하고
remitRepository.withdraw(from, money);
// to계좌에 money 만큼 입금한다
remitRepository.deposit(to, money);
}
}
스프링은 트랜잭션 기능을 어노테이션 하나로 적용 가능하도록 하였는지 지금부터 이 원리와 방법을 알아보자.
2. Transaction without a @Transactional annotation
만약 스프링이 제공하는 Transaction 기능을 @Transactional 없이 코드로 직접 구현해보면 어떻게 될까? 그 기능을 단순하게 알아보자.
@Service
public class RemitService {
private RemitRepository remitRepository;
private DataSource ds;
public void remit(Member from, Member to, int money) {
Connection con = null;
try {
con = ds.getConnection();
con.setAutoCommit(false);
// from의 계좌에서 돈을 money만큼 인출하고
remitRepository.withdraw(from, money);
// to계좌에 money 만큼 입금한다
remitRepository.deposit(to, money);
con.commit();
} catch (Exception e) {
con.rollback();
throw e;
} finally {
if (con != null) {
try {
con.setAutoCommit(true);
con.close();
} catch (Exception e) {
log.error("error: " , e);
}
}
}
}
}
코드가 완성되었다. 실제로는 두 줄에 불과한 코드가 열 줄 넘게 되었을 뿐 아니라 반복적인 코드가 사용되었고 또한 서비스에서 DataSource를 주입받으며 결합도가 높아졌다. 직접 Connection 객체를 닫음으로써 Connection Pool도 고려되지 않았다. 또한 이 소스는 DataSource를 다룰 때고 만약 JPA를 사용한다면 코드가 또 달라진다.
@Service
public class RemitService {
private RemitRepository remitRepository;
private final EntityManager em;
// 여기의 모든 메서드는 Transactional 이 작용된다.
@Transactional
public void remit(Member from, Member to, int money) {
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
// from의 계좌에서 돈을 money만큼 인출하고
remitRepository.withdraw(from, money);
// to계좌에 money 만큼 입금한다
remitRepository.deposit(to, money);
tx.commit();
} catch (Exception e) {
tx.rollback();
throw e;
}
}
}
코드량이 줄어들긴 했지만 역시 EntityManager에 의존하게 되고 try catch를 포함하여 비즈니스 코드보다 트랜잭션을 위한 코드가 더 많아졌다.
스프링은 이것을 어떻게 추상화하여 @Transactional 로 줄였는지 알아보자.
@Transactional의 기능
스프링의 트랜잭션의 핵심은 PlatformTransactionManager을 통해 이루어진다. (리액티브는 ReactiveTransactionManager을 이용한다.) 😊 PlatformTransactionManager.java
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
트랜잭션은 간단히 말해서 동시에 커밋이거나 롤백하는 기능이다. 그래서 트랜잭션 시작지점에 getTransaction으로 상태를 가져오고 commit이나 rollback을 적용하게 해 놓았다. 스프링은 AbstractPlatformTransactionManager 추상 클래스로 getTransaction(), commit(), rollback()을 정해놓고 상속받는 각 TransactionManager마다 doGetTransaction(), doCommit() 등을 오버라이드 하게 하였다. 만약 DataSource를 사용하게 된다면 DataSourceTransactionManager (혹은 JdbcTransactionManager) 가 자동으로 스프링 빈으로 등록되어 사용된다.
😊 DataSourceTransactionManager.java
@Override
protected Object doGetTransaction() {
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
txObject.setSavepointAllowed(isNestedTransactionAllowed());
ConnectionHolder conHolder =
(ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
return txObject;
}
@Override
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
}
try {
con.commit();
}
catch (SQLException ex) {
throw translateException("JDBC commit", ex);
}
}
@Override
protected void doRollback(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
}
try {
con.rollback();
}
catch (SQLException ex) {
throw translateException("JDBC rollback", ex);
}
}
Reference
https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html
Error Experience
- No EntityManager with actual transaction available for current thread - cannot reliably process ‘persist’ call
- 테스트에서 @Transactional 을 선언 안 해줘서 에러
댓글남기기