@Transactional 전파 타입(Propagation) 정리
전파 타입(Propagation) 요약
| 전파 타입 | 기존 트랜잭션 있음 | 기존 트랜잭션 없음 |
|---|---|---|
| REQUIRED | 기존 트랜잭션 참여 | 새 트랜잭션 생성 |
| REQUIRES_NEW | 기존 일시 중지 + 새 트랜잭션 생성 | 새 트랜잭션 생성 |
| SUPPORTS | 기존 트랜잭션 참여 | 트랜잭션 없이 실행 |
| NOT_SUPPORTED | 기존 일시 중지 + 트랜잭션 없이 실행 | 트랜잭션 없이 실행 |
| MANDATORY | 기존 트랜잭션 참여 | 예외 발생 |
| NEVER | 예외 발생 | 트랜잭션 없이 실행 |
@Transactional 전파(Propagation)란?
@Transactional 전파(Propagation)는 트랜잭션 메서드가 호출될 때 현재 트랜잭션 존재 여부에 따라
기존 트랜잭션에 참여할지
새로운 트랜잭션을 생성할지
트랜잭션 없이 실행할지
결정하는 규칙입니다.
예를 들어서, A 메서드에서 B 메서드를 호출할 때
A의 트랜잭션을 그대로 사용할지 아니면 B에서 새로운 트랜잭션을 생성할지를 정할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
@Transactional
public void methodA() {
// A의 트랜잭션이 존재하는 상태
methodB();
}
@Transactional(propagation = Propagation.???)
public void methodB() {
// 전파 타입에 따라 트랜잭션 동작이 달라진다
}
왜 전파 타입을 알아야 하는가?
기본적으로 @Transactional의 전파 타입은 REQUIRED입니다.
1
2
3
4
5
6
/**
* The transaction propagation type.
* <p>Defaults to {@link Propagation#REQUIRED}.
* @see org.springframework.transaction.interceptor.TransactionAttribute#getPropagationBehavior()
*/
Propagation propagation() default Propagation.REQUIRED;
대부분의 경우 기본값(REQUIRED)으로 충분하지만
아래와 같은 상황에서는 전파 타입을 변경해야 합니다.
- 로그 기록 : 비즈니스 로직이 실패해도 로그는 반드시 저장해야 할 때
- 외부 API 호출 : 외부 API 호출 결과를 별도로 관리해야 할 때
전파 타입 종류
REQUIRED (기본값)
기존 트랜잭션이 있으면 참여하고 없으면 새로 생성합니다.
가장 많이 사용되는 기본 전파 타입입니다.
1
2
3
4
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(OrderRequest request) {
orderRepository.save(request.toEntity());
}
주의할 점: 기존 트랜잭션에 참여했을 때 내부 메서드에서 예외가 발생하면 전체 트랜잭션이 롤백됩니다.
REQUIRES_NEW
항상 새로운 트랜잭션을 생성하고, 기존 트랜잭션은 일시 중단합니다.
1
2
3
4
5
// logService
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String message) {
logRepository.save(new Log(message));
}
- 기존 트랜잭션 있음 : 기존 트랜잭션 일시 중단 + 새 트랜잭션 생성
- 기존 트랜잭션 없음 : 새 트랜잭션 생성
1
2
3
4
5
6
7
8
9
@Transactional
public void processOrder(OrderRequest request) {
orderRepository.save(request.toEntity()); // 주문 저장
logService.saveLog("주문 생성"); // 별도 트랜잭션으로 실행
// 여기서 예외 발생해도 로그는 이미 커밋됨
validateOrder(request);
}
즉, REQUIRES_NEW는 기존 트랜잭션과 완전히 독립적이기 때문에 내부 트랜잭션의 커밋/롤백이 외부 트랜잭션에 영향을 주지 않습니다.
REQUIRES_NEW 주의사항
완전히 독립적이기 때문에 에러가 발생해도 이미 커밋되어 롤백 불가능하기 때문에 데이터 정합성 문제를 유발시킬 수 있습니다.
또한, DB커넥션 고갈 위험도 있습니다.
REQUIRES_NEW는 호출되면 기존 트랜잭션의 커넥션을 유지한 상태로 새로운 커넥션을 추가로 획득합니다.
1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void processOrder(OrderRequest request) {
// 커넥션 1개 사용 중
logService.saveLog("주문 생성");
// 기존 커넥션 보유 + 새 커넥션 1개 추가 = 총 2개 점유
notificationService.sendNotification(request);
// sendNotification 메서드도 REQUIRES_NEW 일 때,
// 1개 추가 = 총 3개 점유 가능
}
즉, 하나의 요청이 동시에 2개 이상의 커넥션을 점유하게 됩니다.
너무 자주 사용할 시에는 오버헤드가 발생할 수 있습니다.
1
2
3
4
5
6
7
@Transactional
public void batchProcess(List<Item> items) {
for (Item item : items) {
// 1000개 아이템이면 1000개의 트랜잭션 생성 + 커넥션 획득/반환 반복
itemService.processItem(item); // REQUIRES_NEW
}
}
왜냐하면 트랜잭션을 새로 생성할 때마다 커넥션 획득/반환 비용, 트랜잭션 시작/커밋 비용(BEGIN/COMMIT), 기존 트랜잭션 일시 중단(suspend) 등 오버헤드를 일으켜 성능을 저하시키기 때문입니다.
SUPPORTS
기존 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행합니다.
1
2
3
4
@Transactional(propagation = Propagation.SUPPORTS)
public List<Order> getOrders() {
return orderRepository.findAll();
}
- 기존 트랜잭션 있음 : 기존 트랜잭션에 참여
- 기존 트랜잭션 없음 : 트랜잭션 없이 실행
NOT_SUPPORTED
트랜잭션 없이 실행하고, 기존 트랜잭션이 있으면 일시 중단합니다.
1
2
3
4
5
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendNotification(String userId) {
// 트랜잭션 없이 실행
notificationClient.send(userId, "알림 메시지");
}
- 기존 트랜잭션 있음 : 기존 트랜잭션 일시 중단 + 트랜잭션 없이 실행
- 기존 트랜잭션 없음 : 트랜잭션 없이 실행
NOT_SUPPORTED 도 마찬가지로 REQUIRES_NEW 처럼
기존 트랜잭션이 일시 중단(suspend)되면 해당 커넥션은 반환되지 않고 점유된 채로
존재하므로 주의해야 합니다.
MANDATORY
반드시 기존 트랜잭션이 필수이고, 없으면 예외가 발생합니다.
항상 트랜잭션 내에서만 호출되어야 하는 메서드에 사용하면
실수로 트랜잭션 없이 호출하는 상황을 방지할 수 있습니다.
1
2
3
4
5
6
@Transactional(propagation = Propagation.MANDATORY)
public void deductStock(Long productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow();
product.deductStock(quantity);
}
- 기존 트랜잭션 있음 : 기존 트랜잭션에 참여
- 기존 트랜잭션 없음 :
IllegalTransactionStateException발생
1
2
// 트랜잭션 없이 호출하면 예외 발생!
deductStock(1L, 10); // IllegalTransactionStateException
NEVER
MANDATORY와 정반대 개념으로 트랜잭션이 있으면 예외가 발생하고, 트랜잭션 없이만 실행 가능합니다.
1
2
3
4
@Transactional(propagation = Propagation.NEVER)
public String checkHealthStatus() {
return "OK";
}
- 기존 트랜잭션 있음 :
IllegalTransactionStateException발생 - 기존 트랜잭션 없음 : 트랜잭션 없이 실행