목록으로 돌아가기
Spring의 @Transactional과 Propagation옵션
2025년 12월 31일
3개 태그
AOP
JPA
Spring
Spring의 @Transactional 어노테이션을 완전히 이해하기 위한 가이드입니다. Propagation 옵션부터 내부 구현 원리까지, 실제 프로젝트 코드와 함께 살펴봅니다.
목차
1. @Transactional이란?
1.1. 기본 개념
@Transactional은 Spring에서 제공하는 **선언적 트랜잭션 관리(Declarative Transaction Management)**를 위한 어노테이션입니다.
트랜잭션이란?
- 데이터베이스 작업의 논리적 단위
- 모든 작업이 성공하거나 모두 실패해야 함 (ACID 원칙)
BEGIN→ 작업 수행 →COMMIT또는ROLLBACK
선언적 관리란?
- 코드에 직접 트랜잭션 관리 코드를 작성하지 않음
- 어노테이션으로 선언만 하면 Spring이 자동으로 처리
1.2. 사용 방법
// 클래스 레벨: 모든 public 메서드에 적용 @Service @Transactional class CreditPaymentService { // 모든 public 메서드가 트랜잭션 내에서 실행됨 } // 메서드 레벨: 특정 메서드에만 적용 @Transactional fun approvePayment(...) { // 이 메서드만 트랜잭션 내에서 실행됨 }
2. Propagation 옵션 완전 정리
propagation은 트랜잭션이 이미 존재할 때 새로운 트랜잭션이 어떻게 동작할지를 결정합니다.
2.1. REQUIRED (기본값) ⭐ 가장 많이 사용
동작:
- 트랜잭션이 있으면 → 기존 트랜잭션에 참여
- 트랜잭션이 없으면 → 새로운 트랜잭션 생성
특징:
- 같은 트랜잭션 내에서 실행
- 하나라도 실패하면 전체 롤백
- 가장 일반적으로 사용
@Transactional // propagation = REQUIRED (기본값) fun approvePayment(...) { // 기존 트랜잭션에 참여하거나 새로 생성 paymentRepository.save(...) }
2.2. REQUIRES_NEW ⭐⭐ 독립적인 트랜잭션 필요 시
동작:
- 항상 새로운 트랜잭션 생성
- 기존 트랜잭션이 있어도 독립적인 트랜잭션으로 실행
- 기존 트랜잭션은 일시 중지(suspend)
언제 사용하나?
- 각 단계를 독립적으로 커밋해야 할 때
- Saga 패턴 구현 시
- 부분 커밋이 필요한 경우
@Transactional(propagation = Propagation.REQUIRES_NEW) fun markAsPgApproved(...) { // 항상 새로운 트랜잭션에서 실행 // 즉시 커밋됨! paymentRepository.save(...) }
시각적 설명:
다이어그램 로딩 중...
2.3. SUPPORTS
동작:
- 트랜잭션이 있으면 → 참여
- 트랜잭션이 없으면 → 트랜잭션 없이 실행
사용 예시: 읽기 전용 작업
@Transactional(propagation = Propagation.SUPPORTS) fun readOnlyOperation() { // 트랜잭션이 있으면 참여, 없으면 그냥 실행 }
2.4. MANDATORY
동작:
- 반드시 기존 트랜잭션이 있어야 함
- 없으면
IllegalTransactionStateException발생
사용 예시: 트랜잭션 내에서만 호출되어야 하는 메서드
@Transactional(propagation = Propagation.MANDATORY) fun mustBeInTransaction() { // 반드시 트랜잭션 내에서만 호출되어야 함 }
2.5. NOT_SUPPORTED
동작:
- 트랜잭션을 사용하지 않음
- 기존 트랜잭션이 있으면 일시 중지
사용 예시: 로깅, 캐시 작업 등
@Transactional(propagation = Propagation.NOT_SUPPORTED) fun nonTransactionalOperation() { // 트랜잭션 없이 실행 }
2.6. NEVER
동작:
- 트랜잭션을 사용하지 않음
- 트랜잭션이 있으면 예외 발생
사용 예시: 트랜잭션 내에서 호출되면 안 되는 메서드
@Transactional(propagation = Propagation.NEVER) fun mustNotBeInTransaction() { // 트랜잭션 내에서 호출되면 안 됨 }
2.7. NESTED
동작:
- 중첩 트랜잭션 생성 (Savepoint 사용)
- 부모 트랜잭션이 롤백되면 중첩 트랜잭션도 롤백
- 중첩 트랜잭션이 롤백되어도 부모는 계속 진행 가능
사용 예시: 부분 롤백이 필요한 경우
@Transactional(propagation = Propagation.NESTED) fun nestedOperation() { // Savepoint 생성 // 실패하면 이 지점까지만 롤백 }
2.8. Propagation 옵션 비교
다이어그램 로딩 중...
2.9. Propagation 옵션 비교표
| Propagation | 기존 트랜잭션 | 동작 | 사용 빈도 |
|---|---|---|---|
| REQUIRED (기본) | 있음 | 참여 | ⭐⭐⭐⭐⭐ |
| 없음 | 생성 | ||
| REQUIRES_NEW | 있음 | 새로 생성 (기존 일시 중지) | ⭐⭐⭐⭐ |
| 없음 | 생성 | ||
| SUPPORTS | 있음 | 참여 | ⭐⭐ |
| 없음 | 트랜잭션 없이 실행 | ||
| MANDATORY | 있음 | 참여 | ⭐ |
| 없음 | 예외 발생 | ||
| NOT_SUPPORTED | 있음 | 일시 중지 | ⭐ |
| 없음 | 트랜잭션 없이 실행 | ||
| NEVER | 있음 | 예외 발생 | ⭐ |
| 없음 | 트랜잭션 없이 실행 | ||
| NESTED | 있음 | 중첩 트랜잭션 | ⭐⭐ |
| 없음 | 생성 |
3. 실제 프로젝트에서의 활용
3.1. 문제 상황: REQUIRED의 한계
결제 승인 프로세스를 생각해봅시다:
- PG 승인
- 상태를
PG_APPROVED로 저장 - Credit 발급
- 최종 완료
만약
REQUIRED를 사용하면?
@Transactional fun approvePayment(...) { // 같은 트랜잭션 내에서 실행 creditPaymentStatusService.markAsPgApproved(...) // REQUIRED // Credit 발급 실패 supportersServiceClient.issueCredit(...) // ❌ 예외 // 전체 롤백 → PG_APPROVED 상태도 롤백됨! ❌ // 문제: PG는 승인되었는데 DB에는 기록이 없음! }
문제점: PG는 이미 승인되었는데, DB에는 기록이 없어서 추적이 불가능합니다.
다이어그램 로딩 중...
3.2. 해결책: REQUIRES_NEW 사용
// BasePaymentStatusService.kt @Transactional(propagation = Propagation.REQUIRES_NEW) protected open fun markStatus(...) { // 항상 새로운 트랜잭션에서 실행 val updated = changeStatus(payment) return savePayment(updated) // 즉시 커밋됨 } // CreditPaymentService.kt @Transactional // 메인 트랜잭션 fun approvePayment(...) { try { // Step 1: PG 승인 nicePayService.approvePayment(...) // Step 2: 별도 트랜잭션에서 상태 저장 (REQUIRES_NEW) currentPayment = creditPaymentStatusService.markAsPgApproved(payment, tid) // ↑ 이 시점에 이미 DB에 저장되고 커밋됨! // Step 3: Credit 발급 supportersServiceClient.issueCredit(...) // Step 4: 최종 완료 return paymentRepository.saveCreditPayment(updated) } catch (e: Exception) { // Credit 발급 실패 시 // 이미 PG_APPROVED 상태는 별도 트랜잭션에서 커밋되어 있음 // 따라서 롤백되지 않음! // 보상 트랜잭션 실행 compensatePgApproval(currentPayment, tid) // PG 취소 creditPaymentStatusService.markAsIssueCreditFailed(currentPayment) } }
장점:
- ✅ PG 승인 후 상태가 즉시 저장됨
- ✅ Credit 발급 실패해도 상태는 유지됨
- ✅ 보상 트랜잭션으로 PG 취소 가능
다이어그램 로딩 중...
3.3. Saga 패턴과 REQUIRES_NEW
Saga 패턴이란?
- 분산 트랜잭션을 여러 단계로 나누어 처리
- 각 단계를 독립적으로 커밋
- 실패 시 보상 트랜잭션(Compensation) 실행
다이어그램 로딩 중...
4. 트랜잭션 내부 구현 원리
4.1. AOP 프록시 패턴
Spring의 @Transactional은 **AOP (Aspect-Oriented Programming)**를 기반으로 동작합니다.
다이어그램 로딩 중...
프록시 생성 과정
// 원본 클래스 @Service @Transactional class CreditPaymentService { fun approvePayment(...) { // 비즈니스 로직 } }
Spring이 내부적으로 생성하는 프록시 (의사 코드):
class CreditPaymentServiceProxy( private val target: CreditPaymentService, private val transactionInterceptor: TransactionInterceptor ) : CreditPaymentUseCase { override fun approvePayment(...) { // 1. 트랜잭션 시작 val transactionInfo = transactionInterceptor.createTransactionIfNecessary(...) try { // 2. 실제 메서드 호출 val result = target.approvePayment(...) // 3. 트랜잭션 커밋 transactionInterceptor.commitTransactionAfterReturning(transactionInfo) return result } catch (e: Exception) { // 4. 예외 발생 시 롤백 transactionInterceptor.completeTransactionAfterThrowing(transactionInfo, e) throw e } } }
프록시 확인 방법
@SpringBootTest class TransactionProxyTest { @Autowired private lateinit var creditPaymentService: CreditPaymentService @Test fun checkProxy() { // 프록시인지 확인 println(creditPaymentService.javaClass.name) // 출력: com.sclass.paymentservice.application.service.CreditPaymentService$$SpringCGLIB$$0 // AOP 프록시인지 확인 val isProxy = AopUtils.isAopProxy(creditPaymentService) println("Is Proxy:$isProxy") // true } }
4.2. TransactionSynchronizationManager
트랜잭션 상태를 ThreadLocal에 저장하여 관리합니다.
내부 동작 원리
// TransactionSynchronizationManager 내부 (의사 코드) object TransactionSynchronizationManager { // ThreadLocal로 각 스레드별 트랜잭션 정보 저장 private val resources = ThreadLocal<Map<Any, Any>>() private val synchronizations = ThreadLocal<MutableList<TransactionSynchronization>>() private val currentTransactionName = ThreadLocal<String>() private val currentTransactionReadOnly = ThreadLocal<Boolean>() private val actualTransactionActive = ThreadLocal<Boolean>() fun bindResource(key: Any, value: Any) { val map = resources.get() ?: mutableMapOf() map[key] = value resources.set(map) } fun getResource(key: Any): Any? { return resources.get()?.get(key) } }
실제 사용 예시
@Service class CreditPaymentService { fun approvePayment(...) { // 현재 트랜잭션 확인 val isActive = TransactionSynchronizationManager.isActualTransactionActive() println("Transaction Active:$isActive") // true // 트랜잭션 이름 확인 val name = TransactionSynchronizationManager.getCurrentTransactionName() println("Transaction Name:$name") // 출력: com.sclass...CreditPaymentService.approvePayment } }
4.3. REQUIRES_NEW의 내부 구현
REQUIRES_NEW는 어떻게 독립적인 트랜잭션을 생성할까요?
내부 동작 흐름
// AbstractPlatformTransactionManager 내부 (의사 코드) class AbstractPlatformTransactionManager { fun getTransaction(definition: TransactionDefinition): TransactionStatus { val transaction = doGetTransaction() // 현재 트랜잭션 확인 if (definition.propagation == Propagation.REQUIRES_NEW) { // 기존 트랜잭션이 있으면 일시 중지 if (transaction != null) { val suspendedResources = suspend(transaction) // ThreadLocal에서 트랜잭션 정보 제거 // 새로운 트랜잭션 시작 return startTransaction(definition, suspendedResources) } else { // 트랜잭션이 없으면 새로 생성 return startTransaction(definition, null) } } // ... REQUIRED 등의 처리 } private fun suspend(transaction: Any): SuspendedResourcesHolder { // 1. 현재 트랜잭션 정보 저장 val name = TransactionSynchronizationManager.getCurrentTransactionName() val readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly() val wasActive = TransactionSynchronizationManager.isActualTransactionActive() // 2. ThreadLocal에서 트랜잭션 정보 제거 TransactionSynchronizationManager.unbindResource(dataSource) TransactionSynchronizationManager.clear() // 3. 저장된 정보를 SuspendedResourcesHolder에 보관 return SuspendedResourcesHolder( transaction = transaction, name = name, readOnly = readOnly, wasActive = wasActive ) } private fun resume(holder: SuspendedResourcesHolder) { // 일시 중지된 트랜잭션 재개 TransactionSynchronizationManager.bindResource(dataSource, holder.transaction) TransactionSynchronizationManager.setCurrentTransactionName(holder.name) TransactionSynchronizationManager.setCurrentTransactionReadOnly(holder.readOnly) TransactionSynchronizationManager.setActualTransactionActive(holder.wasActive) } }
REQUIRES_NEW 실행 흐름 시각화
다이어그램 로딩 중...
ThreadLocal 상태 변화 요약:
다이어그램 로딩 중...
4.4. Connection 관리
같은 트랜잭션 내에서의 Connection 재사용
@Transactional fun approvePayment(...) { // 첫 번째 DB 작업 val payment1 = paymentRepository.findCreditPaymentByPgOrderId(...) // Connection1 획득 // 두 번째 DB 작업 val payment2 = paymentRepository.findCreditPaymentById(...) // Connection1 재사용 (같은 트랜잭션) // 세 번째 DB 작업 paymentRepository.saveCreditPayment(...) // Connection1 재사용 } // Connection1.commit() 또는 rollback()
다이어그램 로딩 중...
댓글 (0)
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!