개발
트랜잭션 전파·격리 수준 완전 정복
스택큐힙리스트
2025. 7. 18. 19:10
반응형
1️⃣ 트랜잭션 전파(Propagation)란?
스프링 AOP 프록시가 현재 실행 중인 트랜잭션을 “새로 만들지, 이어받지, 잠깐 끊을지” 결정하는 규칙입니다.
| 전파 옵션 | 의미 (한 줄 요약) |
| REQUIRED (기본) | ❝있으면 참여, 없으면 새로 생성❞ – 80 % 이상 이걸로 OK |
| REQUIRES_NEW | ❝무조건 새 트랜잭션❞ – 외부 트랜잭션과 독립 커밋/롤백 |
| NESTED | ❝논리적 내부 트랜잭션❞ – Savepoint 기반 부분 롤백 가능 |
| SUPPORTS / NOT_SUPPORTED | ❝있으면 따라가고 / 완전히 비트랜잭션❞ |
| MANDATORY / NEVER | ❝반드시 있어야 함 / 있으면 안 됨❞ – Assertion 용 |
실전 팁
- REQUIRES_NEW는 알림 메일·로그 저장처럼 메인 로직 실패와 분리하고 싶을 때.
- NESTED는 배치 처리 중 일부 레코드만 롤백할 때 유용.
- 트랜잭션 경계는 Service 계층에만! Controller나 Repository에 붙이지 말 것.
2️⃣ 트랜잭션 격리 수준(Isolation Level)은 왜 필요할까?
동시에 실행되는 트랜잭션 간 “더티 리드·반복 불가·팬텀 리드” 같은 이상 현상을 얼마나 막을지 정하는 레버입니다.
- READ_UNCOMMITTED : 다른 트랜잭션의 미커밋 데이터를 읽음 → 거의 쓰지 않음
- READ_COMMITTED : 커밋된 데이터만 읽음 (MySQL InnoDB 기본)
- REPEATABLE_READ : 같은 트랜잭션 내에서 같은 결과 보장, 팬텀 리드는 허용
- SERIALIZABLE : 가장 엄격, 사실상 순차 실행 — 성능 부담 大
- DEFAULT : DB 벤더 기본값을 그대로 사용
성능 ↔ 일관성 트레이드오프
👉 OLTP 서비스는 대부분 READ_COMMITTED / REPEATABLE_READ가 안정적.
👉 대량 정산 배치나 회계 처리에는 SERIALIZABLE 고려.
3️⃣ 실전 예제: 전파 + 격리 한 방에 세팅
@Service
@RequiredArgsConstructor
public class PaymentService {
private final OrderRepository orderRepository;
private final PayGateway payGateway;
// 주문 결제 로직 – 메인 트랜잭션
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void placeOrder(OrderRequest req) {
Order order = orderRepository.save(req.toEntity());
try {
// 부수 작업은 새 트랜잭션으로 분리
sendPayRequest(order);
} catch (RuntimeException e) {
log.warn("결제 요청 실패, 주문만 롤백");
throw e; // placeOrder 전체 롤백
}
}
// 결제망 호출 – 실패해도 주문 테이블은 영향 없음
@Transactional(propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.READ_COMMITTED)
public void sendPayRequest(Order order) {
payGateway.request(order);
}
}
- placeOrder() 실패 시 두 트랜잭션 모두 롤백.
- sendPayRequest()만 실패하면 주문 트랜잭션에 영향 없고, 재시도 로직 붙이기 용이.
4️⃣ 흔한 실수
- REQUIRES_NEW 남발 → 커넥션 풀 고갈·Deadlock 가능.
- Service A → Service B 호출 시 각자 @Transactional 기본(REQUIRED) 달아 놓고 “중첩” 되는 줄 착각. 실제로는 단일 트랜잭션!
- Isolation 높이기로 동시성 버그 해결하려다 성능 폭락.
5️⃣ 마무리
- 전파 옵션은 ”왜 분리해야 하는가?”를 먼저 묻고 설정
- 격리 수준은 DB 기본값을 믿되, 비즈니스 요구로 올릴 땐 부하 테스트
- 테스트 코드에서 @Transactional(propagation = Propagation.NOT_SUPPORTED) 로 “진짜 커밋” 상태 검증
반응형