트랜잭션은 가장 작은 작업의 단위로, 우리는 여러개의 SQL이 사용되는 작업을 하나의 트랜잭션으로 취급해야하는 경우가 존재한다. 대표적인 예가 우리가 모두 알고 있는 계좌이체 같은 경우이다. 스프링에서 트랜잭션을 위해 여러가지 기술들을 제공하고 있는데 관련해서 알아보자.
트랜잭션 경계설정
JDBC를 이용하게되면 아래와 같이 트랜잭션 경계를 설정하여 2개의 작업을 한 트랜잭션으로 묶을 수 있게 된다.
Connection c = dataSource.getConnection();
c.setAutoCommit(false); // 트랜잭션 시작 (자동 커밋 옵션 false)
try {
// 하나의 트랜잭션으로 묶인 단위 작업
ProparedStatement st1 = c.prepareStatement("출금계좌에서 이체금액을 뺌");
st1.executeUpdate();
ProparedStatement st2 = c.prepareStatement("입금계좌에서 이체금액만큼 증가시킴");
st2.executeUpdate();
c.commit(); // 트랜잭션 커밋
} catch(Exception e) {
c.rollback(); // 트랜잭셕 롤백
}
c.close();
- JDBC의 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에 일어난다.
- 트랜잭션 경계설정(transaction demacrcation)은 트랜잭션 시작/종료 작업을 설정하는 것을 말한다.
- 로컬 트랜잭션(local transaction)은 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 말한다.
트랜잭션 경계설정작업은 어디에서?
어떤 일련의 작업이 하나의 트랜잭션으로 묶이려면 그 작업이 진행되는 동안 DB커넥션도 하나만 사용돼야 한다. 트랜잭션은 Connection 오브젝트 안에서 만들어지기 때문이다. 일반적으로 DB 커넥션은 DAO에서 다루게 되는데 .. 그렇다고 DAO 안에서 트랜잭션을 관리하게 된다면 비즈니스로직과 데이터 로직이 섞여버리는 어마무시한 결과가 일어날 수 있다. 그렇기 때문에 트랜잭션을 적용하려면 결국 경계설정 작업을 Service
쪽으로 가져와야 한다.
Service의 트랜잭션 경계설정 구조
public void 계좌이체() {
// 1. DB Connection 생성
// 2. 트랜잭션 시작
try {
// 3. DAO 메소드 호출
// 3-1. 출금
// 3-2. 입금
// 4. 트랜잭션 커밋
} catch(Exception e) {
// 5. 트랜잭션 롤백
} finally {
// 6. DB Connection 종료
}
}
Service에서 생성된 Connection은 데이터 엑세스 작업을 진행하는 DAO로 넘겨져야 하는데 그래야 같은 트랜잭션안에서 동작하기 때문이다. 따라서 DAO 메소드들은 Connection을 파라미터로 전달받아야하는 슬픈 처지에 이르게 된다.
Connection를 파라미터로 전달받는 DAO
public interface 계좌DAO {
public void 출금(Connection c, Account account);
public void 입금(Connection c, Account account);
}
이런식으로 수정하면 우리는 트랜잭션 설정에 성공한다. 하지만 마음에 걸리는 부분들이 있다.
- DAO의 메소드와 비즈니스 로직을 담고 있는 Service의 메소드에 Connection 파라미터가 추가되어야 한다는 점
- Connection 파라미터가 DAO 인터페이스에 추가되면 더 이상 데이터 엑세스 기술에 독립적일 수 없다는 점
(데이터 엑세스 기술을 JPA로 변경한다면, EntitiyManager를 전달받도록 DAO, Service를 수정해야 한다.)
등등 ..
스프링에서는 이 방법을 일부 해결하기 위하여 트랜잭션 동기화방식을 제안한다. 트랜잭션 동기화 방식을 사용하면 지저분한 Connection 파라미터 문제를 해결할 수 있다.
트랜잭션 동기화(Transaction Synchronization)
Service에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서는 지정된 Connection을 가져다 사용하는 방식이다.
동작 흐름
- Service에서 Connection 생성 후 트랜잭션 동기화 저장소에 저장
- DAO 에서 트랜잭션 동기화 저장소에 현재 시작된 트랜잭션을 가진 Connection 이 존재하는지 확인 후 SQL 실행(이 후 작업 반복)
- 작업이 끝나면 Service에서 Connection commit/rollback을 호출하여 트랜잭션을 종료시키며, 트랜잭션 저장소에 저장된 동기화된 Connection 오브젝트도 제거(트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection을 저장/관리하기 때문에 멀티 스레드 환경에서도 문제가 없다.)
트랜잭션 동기화 적용
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void 계좌이체() {
// 동기화 시작
TransactionSynchronizeManager.initSynchronization();
Connection c = DataSourceUtils.getConnection(dataSource);
c.setAutoCommit(false);
try{
// 작업 진행
accountDao.출금(account);
accountDao.입금(account);
c.commit();
} catch(Exception e) {
c.rollback();
} finally {
// 동기화 종료
DataSourceUtils.releaseConnection(c, dataSource);
TransactionSynchronizeManager.unbindResource(dataSource);
TransactionSynchronizeManager.clearSynchronization();
}
}
- DataSourceUtils의 getConnection()은 Connection 생성뿐 아니라 트랜잭션 동기화에 사용하도록 저장소에 바인딩을 시켜준다.
그러나 꺼림직한 점이 있다. 해당 트랜잭션 경계설정 로직은 트랜잭션 API에 종속되어 있다는 점이다. 만약에 여러 DB 작업을 하나의 트랜잭션으로 만들어야하는 상황이 오면 JDBC의 Connection을 이용한 방식인 로컬 트랜잭션(Local Transaction)
으로는 해당 작업이 불가능하다. 로컬 트랜잭션은 하나의 DB Connection에 종속되기 때문인데,이 때는 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리하는 글로벌 트랜잭션(Global Transaction)
방식을 사용해야 한다. JTA(Java Transaction API)를 이용하여 트랜잭션 경계설정을 해야하는데 해당 트랜잭션으로 바꾸려면 Service 코드가 수정되어야 한다. 하이버네이트를 이용해야 한다면 어떨까? 하이버네이트는 독자적인 트랜잭션 관리 API를 사용하기 때문에 하이버네이트 경계설정이 적용된 Service가 필요하게 될지도 모른다.
DAO 패턴을 사용해 구현 데이터 엑세스 기술을 유연하게 바꿔 사용할 수 있게 하였지만 Service에 트랜잭션 경계 설정이 필요해지면서 특정 데이터 엑세스 기술에 종속되는 구조가 되고만 것이다. 이러한 문제로 인하여 스프링은 트랜잭션 서비스를 추상화하여 제공한다. 트랜잭션 추상계층이 제공하는 API를 이용해 트랜잭션을 만들어주면 특정 기술에 종속되지 않는 트랜잭션 경계설정 코드를 만들 수 있게 된다.
트랜잭션 서비스 추상화
스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공한다. 이를 이용하면 애플리케이션에서 직접 각 기술의 트랜잭션 API를 이용하지 않고도, 일관된 방식으로 트랜잭션을 제어하는 트랜잭션 경계설정 작업이 가능해진다.
스프링 트랜잭션 추상화 API를 적용한 방법
public void 계좌이체() {
// JDBC 트랜잭션 추상 오브젝트 생성 - 실제로는 bean으로 생성해서 사용
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
// if, JTA사용시 구현체만 변경해주면 된다.
// PlatformTransactionManager transactionManager = new JTATransactionManager();
// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 트랜잭션 안에서 수행되는 작업
accountDao.출금(account);
accountDao.입금(account);
transactionManager.commit(status);
} catch(Exception e) {
transactionManager.rollback(status);
}
}
- 트랜잭션 추상화 기술은 앞에서 적용해봤던 트랜잭션 동기화를 사용한다. PlatformTransactionManager로 시작한 트랜잭션은 트랜잭션 동기화 저장소에 저장된다.
여태까지 스프링에서 제공하는 깔끔한 트랜잭션 인터페이스를 사용했지만 아직까지 비즈니스로직이 주인이어야할 Service안에 트랜잭션 코드가 많은 자리를 차지하게 된다. 트랜잭션의 경계는 분명 비즈니스 로직 전후에 설정되야 하는 것이니 Service 메소드에 두는 것을 거부할 수는 없지만 조금 세련되게 사용할 수 없을까?
AOP를 이용한 선언적 트랜잭션
AOP를 이용하면 트랜잭션 경계설정을 조금 더 세련되고 깔끔하게 사용할 수 있다. @Transactional 어노테이션을 사용하는 방법인데 선언적 트랜잭션(Declarative Transaciton)이라 부르며 AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법이다.
@Transactional
public void 계좌이체() {
// 트랜잭션 안에서 수행되는 작업
accountDao.출금(account);
accountDao.입금(account);
}
선언적 트랜잭션은 나중에 깊게 더 살펴보자.
참고) 토비의 스프링
'프로그래밍 노트 > SPRING' 카테고리의 다른 글
[Spring] 동적 프록시 기술(feat. 리플렉션) (0) | 2022.09.27 |
---|---|
[Spring] 프록시 활용 - 프록시 패턴, 데코레이터 패턴 (0) | 2022.09.26 |
Master/Slave DB 라우팅/이용하기(feat. AbstractRoutingDataSource) (0) | 2022.04.03 |
[SpringBatch] JobParameter와 Scope (0) | 2021.12.01 |
[Spring] 스프링MVC 기본 설정(xml, java config) (0) | 2020.12.21 |