TIL-01: 스프링 프레임워크에서 트랜잭션을 처리하는 방법 - 2

이번 글에서는 지난 글에 이어서 Spring Framework에서 트랜잭션을 어떻게 처리하는지 좀 더 구체적으로 알아보도록 하겠습니다.
Part 4: Spring Framework Transaction
1. Declarative Transaction Management
선언적 vs 프로그래밍 방식
스프링은 두 가지 트랜잭션 관리 방식을 제공합니다:
| 방식 | 설명 | 코드 스타일 |
|---|---|---|
| 프로그래밍 방식 | 코드로 직접 트랜잭션 제어 | transactionManager.begin(), transactionManager.commit() |
| 선언적 방식 | 어노테이션으로 트랜잭션 선언 | @Transactional |
프로그래밍 방식의 문제점
// ❌ 프로그래밍 방식 - 코드가 복잡함
@Service
public class OrderService {
private final TransactionTemplate transactionTemplate;
public void createOrder(Order order) {
transactionTemplate.execute(status -> {
try {
// 비즈니스 로직
orderRepository.save(order);
paymentRepository.save(payment);
return null;
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
});
}
}- 문제점:
- 🔴 트랜잭션 코드와 비즈니스 로직이 섞임
- 🔴 모든 메서드마다 반복 코드
- 🔴 테스트하기 어려움
- 🔴 가독성 저하
선언적 방식의 장점 🌟
// ✅ 선언적 방식 - 깔끔!
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 비즈니스 로직만!!
}
}- 장점:
- ✅ 비즈니스 로직과 트랜잭션 분리
- ✅ 코드 중복 제거
- ✅ 테스트 용이
- ✅ 가독성 향상
- ✅ 유지보수 쉬움
선언적 트랜잭션의 핵심 구성 요소
Declarative Transaction Management
│
│--- 1. @Transactional (선언)
│ "이 메서드는 트랜잭션이 필요해!"
│
│--- 2. AOP Proxy (실행)
│ 프록시가 트랜잭션 로직을 대신 실행
│
│--- 3. Transaction Manager (관리)
│ 실제 트랜잭션을 시작/커밋/롤백
│
│--- 4. Transaction Synchronization Manager (동기화)
트랜잭션 정보를 ThreadLocal에 저장
- 이 4개의 핵심 구성 요소는 매우 중요합니다.
@TransactionalAOP ProxyTransaction ManagerTransaction Synchronization Manager
AOP Proxy는 우리가 이미 살펴봤고 나머지 3개의 개념에 대해 이 글에서 자세히 살펴봅시다.
2. @Transaction 동작 원리
@Transactional 이란? 🎯
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// 트랜잭션 속성들...
}- 한 문장 정의:
@Transactional은 이 메서드를 트랜잭션 안에서 실행하라고 스프링에게 알려주는 Marker 어노테이션입니다. - Github
spring-frameworkrepository에서 직접 Transactional.java 코드를 살펴보고 한 번 (코멘트까지 포함하여) 읽어보시면 더 좋습니다.
@Transactional의 주요 속성들
@Service
public class OrderService {
@Transactional(
propagation = Propagation.REQUIRED, // 전파 속성
isolation = Isolation.DEFAULT, // 격리 수준
timeout = 30, // 타임아웃 (초)
readOnly = false, // 읽기 전용 여부
rollbackFor = Exception.class, // 롤백 대상 예외
noRollbackFor = RuntimeException.class // 롤백하지 않을 예외
)
public void createOrder(Order order) {
orderRepository.save(order);
}
}1️⃣ Propagation (전파 속성) - 가장 중요! ⭐️
문제 상황:
@Transactional
public void methodA() {
// 트랜잭션 A 시작
methodB(); // methodB도 @Transactional인데?
// 트랜잭션을 새로 만들까? 기존 것을 사용할까?
}
@Transactional
public void methodB() {
// ???
}전파 속성 종류:
| 속성 | 의미 | 동작 |
|---|---|---|
| REQUIRED(기본값) | 트랜잭션 필요 | 기존 있으면 참여, 없으면 새로 생성 |
| REQUIRES_NEW | 항상 새 트랜잭션 | 기존 있어도 중단하고 새로 생성 |
| SUPPORTS | 트랜잭션 지원 | 있으면 참여, 없어도 실행 |
| MANDATORY | 트랜잭션 필수 | 기존 있으면 참여, 없으면 예외 |
| NOT_SUPPORTED | 트랜잭션 불필요 | 있으면 중단, 트랜잭션 없이 실행 |
| NEVER | 트랜잭션 금지 | 있으면 예외 |
| NESTED | 중첩 트랜잭션 | 기존 트랜잭션 내 중첩 |
가장 많이 쓰이는 REQUIRED 예시:
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional // REQUIRED (기본값)
public void createOrder(Order order) {
// 트랜잭션 A 시작
orderRepository.save(order);
// paymentService.pay()도 @Transactional(REQUIRED)
// 트랜잭션 A에 참여! (새로 만들지 않음)
paymentService.pay(order.getId());
// 둘 다 트랜잭션 A 안에서 실행됨
// 하나라도 실패하면 둘 다 롤백!
}
}
@Service
public class PaymentService {
@Transactional // REQUIRED
public void pay(Long orderId) {
paymentRepository.save(payment);
}
}- PaymentService.pay 메서드 실행 과정에서 오류가 발생하게 되면 pay 메서드 뿐만 아니라 createOrder 메서드 실행 과정에서 수행된 데이터 처리 작업 역시 전부 롤백 됩니다.
- 모두 하나의 트랜잭션으로 처리되고 있었기 때문입니다.
REQUIRES_NEW 예시:
@Service
public class OrderService {
@Autowired
private LogService logService;
@Transactional // REQUIRED
public void createOrder(Order order) {
// 트랜잭션 A
orderRepository.save(order);
// REQUIRES_NEW -> 트랜잭션 B 새로 생성
logService.log("주문 생성");
// 주문이 실패해서 롤백되어도 로그는 별도 트랜잭션이라 커밋됨!
throw new RuntimeException("주문 실패");
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String message) {
// 트랜잭션 B (독립적)
logRepository.save(log);
// 여기는 항상 커밋됨!
}
}- LogService.log 메서드 실행이 끝나고 그 이후에 createOrder 메서드에서 오류가 발생해도 log 메서드 실행 과정에서 수행된 데이터 처리 작업은 롤백되지 않습니다.
- 서로 다른 독립적인 트랜잭션으로 처리되었기 때문입니다.
- 이렇게 의도적으로 분리된 트랜잭션을 만들어서 원래 작업의 성공 여부에 관계없이 무조건 데이터 처리 작업을 저장하게끔 하도록 할 수 있습니다.
2️⃣ Isolation (격리 수준)
문제: 동시성 이슈
Transaction A Transaction B
│ │
├─ 계좌 조회 (10,000원) │
│ ├─ 계좌 조회 (10,000원)
├─ 5,000원 출금 │
├─ 커밋 (5,000원) │
│ ├─ 3,000원 출금
│ └─ 커밋 (???)
최종 잔액: 2,000원? 7,000원? 🤔
- 하나의 계좌에서 5,000원 출금과 3,000원 출금 작업이 동시에 발생하는 경우입니다.
- Transaction A와 Transaction B가 각각 거의 동시에 원본 금액(10,000원)을 읽고 그 이후에 개별적으로 차감 작업을 하고 있기 때문에 두 Transaction 처리가 모두 끝났을 때, 최종 금액은 7,000원이 됩니다.
- 이는 정상적인 금액인 2,000원과 차이가 나는 것으로서 동시성 이슈가 발생했다고 합니다.
@Transactional 어노테이션의 isolation 속성은 이 동시성 이슈를 처리하기 위한 여러가지 격리 수준을 지정할 수 있게 합니다. 이 글에서는 isolation 이라는 개념에 대해 깊게 다루지 않습니다. isolation은 DB에서 아주 중요한 개념으로서 다른 글에서 따로 자세하게 다뤄볼 예정입니다. 일단 지금은 개발자가 선언적으로 이 isolation의 수준을 지정할 수 있다는 것 정도만 알고 넘어가도록 합시다.
격리 수준:
| 수준 | 설명 | 발생 가능한 문제 |
|---|---|---|
| DEFAULT | DB 기본값 사용 | DB마다 다름 |
| READ_UNCOMMITTED | 커밋 안 된 것도 읽기 | Dirty Read |
| READ_COMMITTED | 커밋된 것만 읽기 | Non-Repetable Read |
| REPEATABLE_READ | 같은 데이터 반복 조회 | Phantom Read |
| SERIALIZABLE | 완전 격리 | 성능 저하 |
3️⃣ readOnly
읽기 전용 최적화:
@Transactional(readOnly = true)
public List<Order> findOrders() {
// 읽기만 함
return orderRepository.findAll();
}- 효과
- ✅ JPA: flush를 하지 않아 성능 향상
- ✅ JDBC: DB가 읽기 전용으로 최적화
- ✅ 실수로 데이터 변경 방지
4️⃣ rollbackFor / noRollbackFor
기본 롤백 규칙:
- ✅ RuntimeException (Unchecked): 롤백
- ❌ Exception (Checked): 커밋
// 기본 동작
@Transactional
public void method1() {
// RuntimeException → 롤백 ✅
throw new RuntimeException("롤백됨");
}
@Transactional
public void method2() throws Exception {
// Checked Exception → 커밋 ❌
throw new Exception("커밋됨?!");
}
// 커스텀 설정
@Transactional(rollbackFor = Exception.class)
public void method3() throws Exception {
// 이제 Checked Exception도 롤백! ✅
throw new Exception("롤백됨");
}- 메서드 실행 과정에서 오류가 발생하면 모든 데이터 처리 작업을 롤백 한다는 것이 트랜잭션의 기본 개념입니다. 여기서 '오류'가 무엇인지 세밀하게 지정할 수 있도록 지원하는 것이
rollbackFor,noRollbackFor속성입니다.
3. Transaction Manager 역할
Transaction Manager란?
Transaction Manager는 트랜잭션을 실제로 시작, 커밋, 롤백하는 핵심 컴포넌트입니다.
PlatformTransactionManager 인터페이스 🔑
스프링의 모든 Transaction Manager는 이 인터페이스를 구현합니다. Github에서 살펴보기
Spring 6.x부터는 TransactionManager 인터페이스가 상위에 추가되었고, PlatformTransactionManager는 이를 확장하도록 변경되었습니다.
// 간소화 된 코드를 통해 이해해봅시다!
public interface PlatformTransactionManager {
// 트랜잭션 시작
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 트랜잭션 커밋
void commit(TransactionStatus status) throws TransactionException;
// 트랜잭션 롤백
void rollback(TransactionStatus status) throws TransactionException;
}- 핵심 메서드 3개:
getTransaction(): 트랜잭션 시작 또는 기존 트랜잭션 참여commit(): 트랜잭션 커밋rollback(): 트랜잭션 롤백
Transaction Manager 구현체 📊
PlatformTransactionManager (인터페이스)
│
├─── DataSourceTransactionManager
│ └─ JDBC, MyBatis 사용 시
│
├─── JpaTransactionManager
│ └─ JPA, Hibernate 사용 시
│
├─── HibernateTransactionManager
│ └─ Hibernate만 사용 시
│
├─── JtaTransactionManager
│ └─ 분산 트랜잭션 (JTA) 사용 시
│
└─── WebLogicJtaTransactionManager
└─ WebLogic 서버에서 JTA 사용 시
🌟 DataSourceTransactionManager
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
// DB Connection Pool 설정
return new HikariDataSource();
}
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}동작 방식:
// 내부 동작 (간소화)
public class DataSourceTransactionManager implements PlatformTransactionManager {
private DataSource dataSource;
@Override
public TransactionStatus getTransaction(TransactionDefinition def) {
// 1. DataSource에서 Connection 획득
Connection conn = dataSource.getConnection();
// 2. 자동 커밋 크기
conn.setAutoCommit(false);
// 3. 격리 수준 설정
conn.setTransactionIsolation(def.getIsolationLevel().value());
// 4. Connection을 ThreadLocal에 저장
TransactionSynchronizationManager.bindResource(dataSource, conn);
return new DefaultTransactionStatus(conn, ...);
}
@Override
public void commit(TransactionStatus status) {
Connection conn = status.getConnection();
conn.commit(); // JDBC commit
conn.close();
}
@Override
public void rollback(TransactionStatus status) {
Connection conn = status.getConnection();
conn.rollback(); // JDBC rollback
conn.close();
}
}🌟 JpaTransactionManager
JPA, Hibernate와 함께 사용:
@Configuration
@EnableJpaRepositories
public class JpaConfig {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
// JPA EntityManager 설정
return new LocalContainerEntityManagerFactoryBean();
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(
entityManagerFactory().getObject()
);
}
}동작 방식:
// 내부 동작 (간소화)
public class JpaTransactionManager implements PlatformTransactionManager {
private EntityManagerFactory emf;
@Override
public TransactionStatus getTransaction(TransactionDefinition def) {
// 1. EntityManager 생성
EntityManager em = emf.createEntityManager();
// 2. JPA 트랜잭션 시작
em.getTransaction().begin();
// 3. EntityManager를 ThreadLocal에 저장
TransactionSynchronizationManager.bindResource(
emf, em
);
return new DefaultTransactionStatus(em, ...);
}
@Override
public void commit(Transaction status) {
EntityManager em = status.getEntityManager();
em.flush(); // 변경사항을 DB에 반영
em.getTransaction().commit(); // JPA commit
em.close();
}
@Override
public void rollback(TransactionStatus status) {
EntityManager em = status.getEntityManager();
em.getTransaction().rollback(); // JPA rollback
em.close();
}
}📊 DataSource vs JPA Transaction Manager 비교
| 항목 | DataSourceTransactionManager | JpaTransactionManager |
|---|---|---|
| 사용 기술 | JDBC, MyBatis | JPA, Hibernate |
| 관리 대상 | Connection | EntityManager |
| 획득 | dataSource.getConnection() | emf.createEntityManager() |
| 시작 | conn.setAutoCommit(false) | em.getTransaction().begin() |
| 커밋 | conn.commit() | em.getTransaction().commit() |
| 롤백 | conn.rollback() | em.getTransaction().rollback() |
| 종료 | conn.close() | em.close() |
- JpaTransactionManager는 DataSource도 함께 지원 합니다. DataSource를 사용하는 JDBC 코드도 같은 트랜잭션에 포함시킬 수 있습니다.
🔃 Transaction Manager의 핵심 역할
Transaction Manager의 3대 책임:
1. 트랜잭션 리소스 관리
├─ Connection 또는 EntityManager 획득
└─ ThreadLocal에 저장 (스레드 안전)
2. 트랜잭션 경계 설정
├─ 시작: begin / setAutoCommit(false)
├─ 종료: commit / rollback
└─ 전파 속성 적용 (REQUIRED, REQUIRES_NEW 등)
3. 예외 변환
└─ DB 예외를 스프링 예외로 변환
4. Transaction Synchronization Manager
ThreadLocal 기반 트랜잭션 동기화:
// 간소화된 코드
public abstract class TransactionSynchronizationManager {
// ThreadLocal로 트랜잭션 리소스 저장
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
// 현재 스레드의 connection 저장
public static void bindResource(Object key, Object value) {
Map<Object, Object> map = resources.get();
if (map == null) {
map = new HashMap<>();
resource.set(map);
}
map.put(key, value);
}
// 현재 스레드의 connection 가져오기
public static Object getResource(Object key) {
Map<Object, Object> map = resources.get();
return map != null ? map.get(key) : null;
}
}왜 ThreadLocal을 사용할까?
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentRepository paymentRepository;
@Transactional
public void createOrder(Order order) {
// 1. Transaction Manager가 Connection 획득
// 2. ThreadLocal에 저장
orderRepository.save(order);
// → Repository가 ThreadLocal에서 같은 Connection 꺼내 씀
paymentRepository.save(payment);
// → 이것도 ThreadLocal에서 같은 Connection 꺼내 씀
// 같은 Connection = 같은 트랜잭션!
}
}- 효과:
- ✅ 같은 스레드 내 모든 Repository가 같은 Connection 사용
- ✅ Connection을 파라미터로 전달할 필요 없음
지금까지 Transaction을 구성하는 각 요소들에 대해 알아봤고 이제부터는 전체 Flow를 살펴보면서 큰 그림을 그려보도록 하겠습니다.
Transaction 시작/커밋/롤백 전체 Flow
🎯 예제 시나리오:
@RestController
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService; // proxy 주입됨
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
orderService.createOrder(request.toOrder());
return ResponseEntity.ok().build();
}
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService; // proxy 주입됨
@Transactional
public void createOrder(Order order) {
// 1. 주문 저장
orderRepository.save(order);
// 2. 결제 처리 (다른 @Transactional 메서드 호출)
paymentService.processPayment(order.getId());
// 3. 재고 감소
order.decreaseStock();
}
}
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentRepository paymentRepository;
@Transactional(propagation = Propagation.REQUIRED)
public void processPayment(Long orderId) {
Payment payment = new Payment(orderId);
paymentRepository.save(payment);
}
}전체 호출 흐름
[1] HTTP 요청
⬇️
[2] OrderController.createOrder()
⬇️
[3] OrderService(프록시).createOrder() ⬅️ 프록시 진입!
⬇️
[4] TransactionInterceptor.invoke()
⬇️
[5] TransactionAspectSupport.invokeWithinTransaction()
⬇️
[6] TransactionManager.getTransaction() ⬅️ 트랜잭션 시작
⬇️
[7] OrderService(원본).createOrder() ⬅️ 실제 메서드 실행
⬇️
[8] orderRepository.save()
⬇️
[9] PaymentService(프록시).processPayment() ⬅️ 중첩 호출
⬇️
[10] 기존 트랜잭션 참여 (REQUIRED)
⬇️
[11] paymentRepository.save()
⬇️
[12] 메서드 완료
⬇️
[13] TransactionManager.commit() ⬅️ 트랜잭션 커밋
단계 별 상세 분석
HTTP 요청부터 트랜잭션이 커밋되는 전 과정을 이제 하나씩 상세하게 살펴봅시다. 예제 코드를 기준으로 설명할 것입니다. 이 예제 코드는 실제 Spring Framework 코드를 간소화 한 버전으로서 이해를 돕기 위해 작성되었습니다.
Phase 1: 프록시 진입
// [3] 프록시 메서드 호출
orderService.createOrder(order);
// 실제로는 이렇게 동작:
OrderService$$EnhancerBySpringCGLIB$$123412344.createOrder(order) {
// MethodInvocation 생성
MethodInvocation invocation = createMethodInvocation(
"createOrder",
new Object[]{order}
);
// [4] TransactionInterceptor에게 위임
return interceptor.invoke(invocation);
}Phase 2: TransactionInterceptor 실행
// [4] TransactionInterceptor.invoke()
public class TransactionInterceptor extends TransactionAspectSupport {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 1. 대상 클래스 정보
Class<?> targetClass = invocation.getThis().getClass();
// 2. 호출할 메서드
Method method = invocation.getMethod();
// [5] 트랜잭션 내에서 메서드 실행
return invokeWithinTransaction(
method,
targetClass,
invocation::proceed
)
}
}- 실제 클래스 코드는 여기를 참조하세요.
Phase 3: 트랜잭션 속성 파싱
// [5] TransactionAspectSupport.invokeWithinTransaction()
protected Object invokeWithinTransaction(
Method method,
Class<?> targetClass,
InvocationCallback invocation
) throws Throwable {
// 1. @Transactional 속성 읽기
TransactionAttribute txAttr = getTransactionAttributeSource()
.getTransactionAttribute(method, targetClass);
// txAttr 내용:
// - propagation: REQUIRED
// - isolation: DEFAULT
// - timeout: -1 (무제한)
// - readOnly: false
// - rollbackFor: [RuntimeException.class]
// 2. TransactionManager 찾기
PlatformTransactionManager tm = determineTransactionManager(txAttr);
// 3. 트랜잭션 내에서 실행
String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
return createTransactionIfNecessary(tm, txAttr, joinpointIdentification)
.execute(status -> {
return invocation.proceedWithInvocation();
});
}- 실제 구현 코드는 이곳을 참조하세요.
Phase 4: 트랜잭션 시작
// [6] TransactionManager.getTransaction()
public class DataSourceTransactionManager {
@Override
public TransactionStatus getTransaction(TransactionDefinition definition) {
// 1. 기존 트랜잭션이 있는지 확인
Object transaction = doGetTransaction();
// 2. 기존 트랜잭션 확인
if (isExistingTransaction(transaction)) {
// 전파 속성에 따라 처리
return handleExistingTransaction(definition, transaction);
}
// 3. 새 트랜잭션 시작 필요해
return startNewTransaction(definition, transaction);
}
private TransactionStatus startNewTransaction(
TransactionDefinition definition,
Object transaction
) {
// 1. Connection 획득
Connection conn = dataSource.getConnection();
// 2. 자동 커밋 끄기
conn.setAutoCommit(false);
// 3. 격리 수준 설정
if (definition.getIsolationLevel() != Isolation.DEFAULT) {
conn.setTransactionIsolation(
definition.getIsolationLevel().value()
);
}
// 4. ThreadLocal에 Connection 저장
TransactionSynchronizationManager.bindResource(
dataSource,
new ConnectionHolder(conn)
);
// 5. TransactionStatus 반환
return new DefaultTransactionStatus(
transaction,
true, // newTransaction = true
conn,
definition
);
}
}현재 상태:
Thread: http-nio-8080-exec-1
│
├─ ThreadLocal<Map<DataSource, ConnectionHolder>>
│ └─ HikariDataSource → ConnectionHolder
│ └─ Connection (autoCommit=false) ✅
│
└─ TransactionStatus
├─ transaction: DataSourceTransaction
├─ newTransaction: true
├─ connection: HikariProxyConnection@123abc
└─ completed: false
Phase 5: 실제 메서드 실행
// [7] 원본 메서드 실행
orderService.createOrder(order);
public void createOrder(Order order) {
orderRepository.save(order);
// [8] orderRepository.save()
// Repository 내부에서 Connection 가져오기:
Connection conn = TransactionSynchronizationManager.getResource(dataSource);
// 같은 Connection으로 SQL 실행
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO orders (id, amount) VALUES (?, ?)"
);
stmt.setLong(1, order.getId());
stmt.setInt(2, order.getAmount());
stmt.executeUpdate(); // 아직 커밋은 안됨!
// [9] PaymentService 호출
paymentService.processPayment(order.getId());
}Phase 6: 중첩 트랜잭션 처리 (REQUIRED)
// [9] PaymentService(프록시).processPayment() 호출
// ➡️ TransactionInterceptor 다시 실행
public TransactionStatus getTransaction(TransactionDefinition definition) {
// 1. 기존 트랜잭션 확인
ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (holder != null && holder.isTransactionActive()) {
// 기존 트랜잭션 있음 ✅
// 2. REQUIRED 속성 확인
if (definition.getPropagationBehavior() == PROPAGATION_REQUIRED) {
// 기존 트랜잭션에 참여
return new DefaultTransactionStatus(
transaction,
false, // newTransaction = false (새 트랜잭션 아님!)
holder.getConnection(),
definition
)
}
}
}중요 포인트:
OrderService.createOrder() [트랜잭션 A]
⬇️
orderRepository.save() [트랜잭션 A 사용]
⬇️
PaymentService.processPayment() [트랜잭션 A에 참여] ⭐️
⬇️
paymentRepository.save() [트랜잭션 A 사용]
⬇️
모두 같은 Connection, 같은 트랜잭션!
하나라도 실패하면 전부 롤백!
Phase 7: 메서드 완료 커밋
// [12] createOrder() 메서드 정상 완료
// ➡️ TransactionInterceptor로 돌아옴
protected Object invokewithinTransaction(...) {
try {
// 메서드 실행
Object result = invocation.proceedWithInvocation();
// [13] 성공 시 커밋
commitTransactionAfterReturning(txInfo);
return result;
} catch (Throwable ex) {
// 실패 시 롤백
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
}
// [13] 커밋 실행
private void commitTransactionAfterReturning(TransactionInfo txInfo) {
if (txInfo.getTransactionStatus().inNewTransaction()) {
// 이 메서드가 트랜잭션을 시작했다면 커밋
txInfo().getTransactionManager().commit(txInfo.getTransactionStatus());
}
// 참여한 경우 (REQUIRED) ➡️ 커밋하지 않음
}
// TransactionManager.commit()
public void commit(TransactionStatus status) {
if (status.isCompleted()) {
throw new IllegalTransactionStateException("이미 완료된 트랜잭션");
}
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
// 롤백 마크가 있는지 확인
if (defStatus.isLocalRollbackOnly()) {
rollback(status);
return;
}
// 실제 커밋
processCommit(defStatus);
}
private void processCommit(DefaultTransactionStatus status) {
try {
// 1. Connection.commit() 호출
Connection conn = status.getConnection();
conn.commit(); ✅
// 2. TransactionSynchronization 콜백 호출
triggerAfterCommit();
} finally {
// 3. 리소스 정리
cleanupAfterCompletion(status);
}
}
private void cleanupAfterCompletion(TransactionStatus status) {
// 1. ThreadLocal에서 Connection 제거
TransactionSynchronizationManager.unbindResource(dataSource);
// 2. Connection 반환 (➡️ 커넥션 풀)
Connection conn = status.getConnection();
DataSourceUtils.releaseConnection(conn, dataSource);
}최종 상태:
Thread: http-nio-8080-exec-1
│
├─ ThreadLocal<Map> → (비어있음, 정리됨) ✅
│
└─ DB 상태
├─ orders 테이블: INSERT 커밋됨 ✅
└─ payments 테이블: INSERT 커밋됨 ✅
지금 우리가 살펴본 시나리오는 orders, payments 테이블에 데이터를 추가하는 2개의 서로 다른 작업이 하나의 공통된 트랜잭션에서 처리되는 것입니다.
모든 작업이 성공적으로 완료되고 마지막 최종 단계에서 commit 하는 것을 볼 수 있었습니다.
Transaction 롤백 시나리오
이번에는 트랜잭션 처리 과정에서 롤백이 발생하는 과정에 대해 살펴봅시다.
🔴 예외 발생 케이스
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
// 예외 발생! 💣
if (order.getAmount() > 10000) {
throw new RuntimeException("금액 초과");
}
paymentService.processPayment(order.getId());
}롤백 플로우
protected Object invokeWithinTransaction(...) {
try {
Object result = invocation.proceedWithInvocation(); // 예외 발생!
commitTransactionAfterReturning(txInfo);
return result;
} catch (Throwable ex) {
// [예외 캐치!]
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
}
private void completeTransactionAfterThrowing(
TransactionInfo txiInfo, Throwable ex
) {
// 1. 롤백해야 하는 예외인지 확인
if (txInfo.transactionAttribute.rollbackOn(ex)) {
// RuntimeException -> true (기본값)
// Exception -> false (기본값)
// 2. 롤백 실행
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
} else {
// 롤백하지 않고 커밋
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}
// TransactionManager.rollback()
public void rollback(TransactionStatus status) {
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
processRollback(defStatus);
}
private void processRollback(DefaultTransactionStatus status) {
try {
// 1. Connection.rollback() 호출
Connection conn = status.getConnection();
conn.rollback(); 🔄
// 2. TransactionSynchronization 콜백
triggerAfterCompletion(STATUS_ROLLED_BACK);
} finally {
// 3. 리소스 정리
cleanupAfterCompletion(status);
}
}결과:
DB 상태:
├─ orders 테이블: 변경사항 없음 (롤백됨) 🔄
└─ payments 테이블: 변경사항 없음 (실행조차 안됨)
핵심 컴포넌트 관계도
┌─────────────────────────────────────────────┐
│ Client (Controller) │
└─────────────────┬───────────────────────────┘
│ 호출
⬇️
┌─────────────────────────────────────────────┐
│ Proxy (CGLIB 생성) │
│ ┌─────────────────────────────────────┐ │
│ │ TransactionInterceptor │ │
│ │ ├─ @Transactional 속성 읽기 │ │
│ │ └─ TransactionManager 호출 │ │
│ └─────────────────────────────────────┘ │
└─────────────────┬───────────────────────────┘
│
⬇️
┌─────────────────────────────────────────────┐
│ PlatformTransactionManager │
│ ├─ getTransaction() (트랜잭션 시작) │
│ ├─ commit() (커밋) │
│ └─ rollback() (롤백) │
└─────────────────┬───────────────────────────┘
│
⬇️
┌─────────────────────────────────────────────┐
│ TransactionSynchronizationManager │
│ └─ ThreadLocal<Connection> │
└─────────────────┬───────────────────────────┘
│
⬇️
┌─────────────────────────────────────────────┐
│ DataSource (Connection Pool) │
│ └─ Connection (실제 DB 연결) │
└─────────────────────────────────────────────┘
이번 글에서는 Spring Framework에서 트랜잭션을 구체적으로 어떻게 처리하는지 살펴봤습니다.
Database와 관련해서 트랜잭션은 매우 중요한 개념이면서 동시에 복잡합니다. 그래서 이 개념을 제대로 잘 이해하는 것이 아주 중요합니다. 이번 글을 통해 트랜잭션이 무엇인지 왜 처리해야 하는지 그리고 어떻게 처리되는지 대략적으로 감을 잡으셨기를 바랍니다.
그럼 끝까지 읽어주셔서 감사합니다!
Comments (0)
Checking login status...
No comments yet. Be the first to comment!
