
1. XML AOP를 이용한 트랜잭션
레거시 Spring 프로젝트에서 자주 볼 수 있는 방식이다.
Java 코드에 @Transactional을 직접 선언하지 않고 XML에서 어떤 클래스의 어떤 메소드에 트랜잭션을 적용할지
규칙으로 정의한다. 예를 들어 다음과 같은 설정이 있다고 하자
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* *..service.*Service.*(..))"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="insert*" rollback-for="Exception"/>
<tx:method name="update*" rollback-for="Exception"/>
<tx:method name="delete*" rollback-for="Exception"/>
</tx:attributes>
</tx:advice>
그리고 Service에 다음 메소드가 있다고 하자
public class MemberServiceImpl implements MemberService {
public void updateMember(Member member) {
memberDAO.updateMember(member);
historyDAO.insertHistory(member);
}
}
updateMember()에는 @Transactional이 없다.
하지만 XML을 보면 Service가 AOP 대상이다.
pointcut="execution(* *..service.*Service.*(..))"
그리고 메소드 이름도
<tx:method name="update*" rollback-for="Exception"/>
에 해당한다.
따라서 실제 실행 구조는 개념적으로 다음과 같다.
호출자
↓
Spring AOP Proxy
↓
Transaction BEGIN
↓
MemberService.updateMember()
├─ memberDAO.updateMember()
└─ historyDAO.insertHistory()
↓
Transaction COMMIT
중간에 rollback 대상 예외가 발생하면
Transaction BEGIN
↓
updateMember()
├─ updateMember()
└─ insertHistory() → Exception
↓
Transaction ROLLBACK
이 된다. @Transactional과 상당히 비슷하다.
위 XML 설정은 개념적으로 다음과 비슷한 역할을 한다.
@Transactional(transactionManager = "transactionManager", rollbackFor = Exception.class)
public void updateMember(Member member) {
memberDAO.updateMember(member);
historyDAO.insertHistory(member);
}
즉, XML AOP와 @Transactional은 둘 다 Spring이 트랜잭션의 시작, commit, rollback을 대신 관리하는
선언적 트랜잭션(Declarative Transaction) 방식이다. 차이는 주로 트랜잭션 규칙을 어디에 선언하느냐에 있다.
2. @Transactional을 이용한 트랜잭션
현대 Spring/Spring Boot 프로젝트에서 가장 익숙한 형태다.
@Transactional(transactionManager = "transactionManager", rollbackFor = Exception.class)
public void insertDevices(List<Device> devices) {
for (Device device : devices) {
deviceDAO.insert(device);
}
}
Spring은 해당 Bean을 Proxy로 감싸고 외부에서 메소드가 호출될 때 트랜잭션을 시작한다.
호출자
↓
Spring Proxy
↓
Transaction BEGIN
↓
insertDevices()
├─ insert
├─ insert
├─ insert
└─ ...
↓
정상 종료
↓
COMMIT
예외가 발생하고 rollback 조건을 만족하면
insertDevices()
↓
Exception
↓
ROLLBACK
XML AOP와 무엇이 다른가?
동작 원리 측면에서는 둘 다 Spring의 선언적 트랜잭션이라는 공통점이 크다.
| 구분 | XML AOP | @Transactional |
| 방식 | 선언적 | 선언적 |
| Spring Proxy | 사용 | 사용 |
| BEGIN / COMMIT / ROLLBACK | Spring이 처리 | Spring이 처리 |
| 설정 위치 | XML | Java |
| 적용 대상 | Pointcut + 메소드 패턴 | Annotation 대상 |
| 코드에서 가시성 | 상대적으로 낮음 | 높음 |
따라서 다음 코드만 봤을 때
public void updateMember() {
...
}
XML AOP 방식에서는 XML까지 확인해야 이 메소드가 트랜잭션인지 알 수 있다.
반면,
@Transactional
public void updateMember() {
...
}
Annotation 방식은 Java 코드만 봐도 의도가 바로 드러난다.
하지만 이것은 주로 선언 방식의 차이다.
둘 모두 일반적인 사용에서는
메소드 진입 → Transaction 시작 → 메소드 종료 → Commit
이라는 트랜잭션 경계를 만들 수 있다.
3. Programmatic Transaction
세 번째 방식은 성격이 조금 다르다.
Spring이 메소드 전체를 자동으로 감싸도록 선언하는 것이 아니라,
개발자가 코드 안에서 트랜잭션 시작과 종료 시점을 직접 제어한다.
예를 들어 MemberDAOImpl에서 PlatformTransactionManager를 직접 사용한다고 가정해보자
public class MemberDAOImpl implements MemberDAO {
private PlatformTransactionManager transactionManager;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
// ...
}
즉, transactionManager는 MemberDAOImpl이 직접 가지고 사용하는 객체다.
XML Bean 설정을 사용하는 프로젝트라면 MemberDAOImpl 객체에 transactionManager를 주입해주어야 한다.
<bean id="memberDAO" class="com.example.member.dao.MemberDAOImpl">
<property name="transactionManager" ref="transactionManager"/>
</bean>
여기서 ref="transactionManager"는 Spring에 등록되어 있는 PlatformTransactionManager 구현체를 참조한다.
예를 들어 공통 설정에 다음과 같이 등록되어 있을 수 있다.
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
결국 객체 관계는 다음과 같다.
Spring Container
transactionManager Bean
(DataSourceTransactionManager)
│
│ property 주입
▼
MemberDAOImpl
│
└─ private PlatformTransactionManager transactionManager;
이제 MemberDAOImpl에서는 주입받은 transactionManager를 이용해 트랜잭션을 직접 제어할 수 있다.
public class MemberDAOImpl implements MemberDAO {
private PlatformTransactionManager transactionManager;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void updateMembers(List<Member> members) {
TransactionStatus status = null;
try {
// Transaction 시작
status = transactionManager.getTransaction(new DefaultTransactionDefinition());
for (Member member : members) {
// DB 처리
updateMember(member);
}
// Transaction 확정
transactionManager.commit(status);
} catch (Exception e) {
// Transaction 취소
if (status != null && !status.isCompleted()) {
transactionManager.rollback(status);
}
throw e;
}
}
}
이 방식에서는 개발자가 코드 안에서 직접
getTransaction()
↓
Transaction BEGIN
DB 작업
commit()
↓
Transaction COMMIT
또는 실패 시
Exception
↓
rollback()
↓
Transaction ROLLBACK
의 위치를 결정한다.
따라서 XML의
<property name="transactionManager" ref="transactionManager"/>
는 MemberDAOImpl에 트랜잭션을 자동 적용하라는 의미가 아니다.
단순히 MemberDAOImpl이 코드에서 직접 사용할 transactionManager 객체를 주입하는 설정이다.
이 점이 @Transactional이나 XML AOP 방식과 중요한 차이다.
※ Programmatic Transaction은 언제 필요한가?
대표적인 사례가 대량 Batch 처리에서 chunk별로 트랜잭션을 끊어야 하는 경우다.
10만 건의 데이터를 처리한다고 가정해보자
단순히 메소드 전체에
@Transactional
public void executeBatch(List<Data> list) {
for (Data data : list) {
...
}
}
를 적용하면 기본적인 구조는
BEGIN
1
2
3
...
100,000
COMMIT
이 된다.
즉, 10만 건 전체가 하나의 트랜잭션이다.
하지만 업무 요구사항이
"1,000건 단위로 처리하고 1,000건마다 commit한다."
라면 경계가 달라져야 한다.
BEGIN
1 ~ 1,000
COMMIT
BEGIN
1,001 ~ 2,000
COMMIT
BEGIN
2,001 ~ 3,000
COMMIT
...
BEGIN
99,001 ~ 100,000
COMMIT
이런 경우 Programmatic Transaction을 사용할 수 있다.
예를 들면
TransactionStatus status = null;
int count = 0;
for (Data data : list) {
if (status == null) {
status = transactionManager.getTransaction(
new DefaultTransactionDefinition()
);
}
batchSqlSessionTemplate.insert(
"batch.insertData",
data
);
count++;
if (count % 1000 == 0) {
batchSqlSessionTemplate.flushStatements();
transactionManager.commit(status);
status = null;
}
}
// 마지막 1000건 미만 처리
if (status != null) {
batchSqlSessionTemplate.flushStatements();
transactionManager.commit(status);
}
이 코드에서는 개발자가 명확하게
1000건
↓
flush
↓
commit
↓
새 Transaction
1000건
↓
flush
↓
commit
이라는 경계를 만든다.
※ 선언적 트랜잭션과 Programmatic Transaction의 핵심 차이
결국 가장 중요한 차이는 트랜잭션 경계를 얼마나 직접적으로 제어하느냐다.
<선언적 방식>
XML AOP나 @Transactional을 사용하면 보통
┌──────── Transaction ────────┐
│ │
│ public method() │
│ │
│ SQL │
│ SQL │
│ SQL │
│ │
└─────────────────────────────┘
↓
COMMIT
처럼 업무 메소드 단위로 트랜잭션을 표현하기 좋다.
코드도 단순하다.
@Transactional
public void process() {
dao.updateA();
dao.updateB();
dao.updateC();
}
Spring이 나머지를 담당한다.
<Programmatic 방식>
반면 Programmatic Transaction은
method()
│
├── BEGIN
│ ├── 1 ~ 1000
│ └── COMMIT
│
├── BEGIN
│ ├── 1001 ~ 2000
│ └── COMMIT
│
├── BEGIN
│ ├── 2001 ~ 3000
│ └── COMMIT
│
└── ...
처럼 하나의 메소드 내부에서도 여러 개의 트랜잭션 경계를 만들기 쉽다.
이것이 가장 큰 차이다.
※ 그렇다면 Programmatic Transaction이 더 좋은가?
그렇지는 않다.
세밀하게 제어할 수 있는 만큼 개발자가 책임져야 할 것도 많아진다.
예를 들어
try {
status = transactionManager.getTransaction(...);
...
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
처럼 작성했는데 예외를 다시 던지지 않는다면
DB 처리 실패
↓
ROLLBACK
↓
catch
↓
로그만 기록
↓
메소드 정상 return
↓
상위 Scheduler
↓
"정상 종료"
이처럼 DB는 rollback됐는데 상위 시스템은 성공으로 인식하는 문제가 발생할 수도 있다.
따라서 일반적인 Service transaction이라면 선언적 방식을 사용하는 것이 코드도 간결하고 실수 가능성도 낮다.
Programmatic 방식은 트랜잭션 경계를 직접 제어해야 할 명확한 이유가 있을 때 사용하는 것이 좋다.
※ Batch에서 flush와 commit은 다른 동작
MyBatis ExecutorType.BATCH를 사용한다면 이것도 구분해야 한다.
batchSqlSessionTemplate.flushStatements();
transactionManager.commit(status);
이 둘은 같은 동작이 아니다.
개념적으로
batchSqlSessionTemplate.insert()
batchSqlSessionTemplate.insert()
batchSqlSessionTemplate.insert()
↓
flushStatements()
↓
쌓여 있던 JDBC Batch SQL 실행
↓
transactionManager.commit()
↓
DB Transaction 확정
이다. 따라서
flushStatements()를 호출했으니 commit됐다. 라고 이해하면 안 된다.
트랜잭션 내부에서 flushStatements()를 수행했다면 이후 문제가 발생했을 때 해당 transaction이 rollback될 수 있다.
이 차이는 MyBatis Batch를 다룰 때 특히 중요하다.
※ 세 가지 방식을 한눈에 정리하면
| 구분 | XML AOP | @Transactional | Programmatic |
| 분류 | 선언적 | 선언적 | 명령형 / 프로그래밍 방식 |
| 트랜잭션 관리 | Spring | Spring | 개발자 |
| 시작 / 종료 | Proxy가 처리 | Proxy가 처리 | 코드에서 직접 처리 |
| 일반적인 경계 | 메소드 | 메소드 | 원하는 위치 |
| Java 코드 가시성 | 낮음 | 높음 | 매우 높음 |
| Commit 직접 호출 | X | X | O |
| Rollback 직접 호출 | X | X | O |
| Chunk별 Commit 제어 | 상대적으로 부적합 | 별도 구조 필요 | 적합 |
| 구현 복잡도 | 보통 | 낮음 | 높음 |
| 대표 사용처 | 레거시 Spring XML | 일반 Service 업무 | 특수 Batch / 세밀한 경계 |
여기서 중요한 것은 XML AOP와 @Transactional을 서로 완전히 다른 트랜잭션 기술로 볼 필요가 없다는 것이다.
둘은 모두 Spring의 선언적 트랜잭션이며
[선언적 트랜잭션]
XML AOP → Spring Proxy → TransactionManager → BEGIN / COMMIT / ROLLBACK
@Transactional → Spring Proxy → TransactionManager → BEGIN / COMMIT / ROLLBACK
[프로그래밍 방식 트랜잭션]
Programmatic → TransactionManager 직접 호출
→ getTransaction() / commit() / rollback()
이런 차이로 이해하면 쉽다.
※ 실무에서는 무엇을 선택해야 할까?
일반적인 Service 업무라면 다음처럼 @Transactional을 사용하는 것이 가장 이해하기 쉽다.
@Transactional(rollbackFor = Exception.class)
public void updateOrder() {
orderDAO.updateOrder();
paymentDAO.updatePayment();
historyDAO.insertHistory();
}
기존 레거시 프로젝트가 XML AOP 기반으로 잘 구성되어 있다면 굳이 모든 코드를 @Transactional로 바꿀 필요는 없다.
<tx:method name="update*" rollback-for="Exception"/>
같은 기존 transaction contract가 정상적으로 작동한다면 유지할 수 있다.
반면 다음처럼 하나의 메소드 안에서 트랜잭션을 여러 번 시작하고 종료해야 하는 명확한 요구사항이 있다면
Programmatic Transaction을 검토할 수 있다.
대량 데이터
1,000건 → commit
1,000건 → commit
1,000건 → commit
...
즉 선택 기준은 단순히
"어떤 방식이 더 최신인가?"
가 아니라,
"이 업무에서 필요한 트랜잭션 경계가 어디인가?"
가 되어야 한다.
※ 마무리
Spring 트랜잭션을 처음 접하면 @Transactional 자체가 트랜잭션이라고 생각하기 쉽다.
하지만 실제로 중요한 것은 그 뒤에 있는 Transaction Boundary다.
어디서 Transaction을 시작하고
어떤 작업을 하나의 업무 단위로 묶고
어디서 Commit하며
실패했을 때 어디까지 Rollback할 것인가?
XML AOP와 @Transactional은 이러한 경계를 선언적으로 Spring에게 맡기는 방법이고,
Programmatic Transaction은 그 경계를 개발자가 직접 제어하는 방법이다.
특히 Batch 시스템에서는 처리량이 많다는 이유만으로 무조건 트랜잭션을 잘게 나누는 것도,
반대로 메소드 전체를 하나의 트랜잭션으로 묶는 것도 정답은 아니다.
트랜잭션 방식보다 먼저 업무적으로 보장해야 하는 원자성의 범위를 결정하고,
그 범위에 맞는 Transaction Boundary를 선택하는 것이 핵심이다.
'Backend' 카테고리의 다른 글
| [Backend] 트래픽 폭주를 막는 4가지 방법 (Rate Limiting, Caching, Asynchronous Processing, Load Balancing) (0) | 2026.04.25 |
|---|---|
| [Backend] MSA 환경에서 반드시 고려해야 할 핵심 이슈와 해결 방법 (0) | 2025.10.31 |