Saga 패턴으로 분산 트랜잭션 처리하기: Payment Service 실전 사례
마이크로서비스 환경에서 여러 서비스를 거치는 결제 프로세스를 Saga 패턴으로 구현했습니다. PG 승인, Order 생성, 완료 처리의 각 단계에서 실패 시 보상 트랜잭션을 실행하여 데이터 일관성을 유지하는 실전 사례를 공유합니다.
Sample code서론
마이크로서비스에서 여러 서비스를 거치는 비즈니스 로직을 만들 때 가장 까다로운 부분이 분산 트랜잭션입니다.
단일 데이터베이스에서는 ACID 트랜잭션이 잘 동작합니다. 하지만 마이크로서비스 환경에서는 각 서비스가 독립적인 데이터베이스를 가지기 때문에 2PC(Two-Phase Commit) 같은 분산 트랜잭션 프로토콜을 쓰기 어렵습니다.
이 글에서는 실제 프로젝트에서 Saga 패턴을 써서 분산 트랜잭션을 처리한 경험을 공유합니다.
1. 문제 상황: Payment Service의 분산 트랜잭션
시나리오: 결제 승인 프로세스
결제 서비스에서 결제를 승인하는 과정은 세 단계로 나뉩니다:
1. PG(Payment Gateway) 승인
↓
2. 외부 서비스 호출 (Order 생성 또는 Credit 발급)
↓
3. 결제 완료 처리
문제는 이겁니다:
- Step 1 (PG 승인)이 성공했는데 Step 2 (Order 생성)가 실패하면?
- Step 2가 성공했는데 Step 3이 실패하면?
- 각 단계가 다른 서비스나 외부 API를 호출하므로 전통적인 트랜잭션 롤백이 불가능합니다.
![]()
계좌에서 돈은 빠져나갔는데 주문은 안 들어간 상황. 고객 입장에서는 “돈만 받고 물건은 안 주는 거냐”는 불만이 당연합니다. 이런 일을 막기 위해 Saga 패턴이 필요합니다.
전통적인 접근 방식의 한계
// ❌ 이렇게 하면 안 됨 - 분산 환경에서는 불가능
@Transactional
fun approvePayment(...) {
pgService.approve() // 외부 API
orderService.create() // 다른 서비스
paymentRepository.save() // 로컬 DB
// 하나라도 실패하면 전체 롤백? → 불가능!
}
왜 불가능한가?
- PG 서비스는 외부 API라서 우리가 제어할 수 없습니다.
- Order 서비스는 다른 마이크로서비스로 독립적인 DB를 가집니다.
- 각 단계가 이미 커밋된 상태라서 롤백이 불가능합니다.
2. Saga 패턴 소개
Saga 패턴이란?
Saga 패턴은 긴 트랜잭션을 여러 개의 작은 트랜잭션으로 나누고, 각 트랜잭션이 실패하면 보상 트랜잭션(Compensating Transaction)을 실행해서 이전 단계들을 되돌리는 패턴입니다.
Saga 패턴 흐름도
sequenceDiagram
participant Client
participant Orchestrator as PaymentService<br/>(Orchestrator)
participant PG as PG Service
participant Order as Order Service
Client->>Orchestrator: 결제 승인 요청
Note over Orchestrator: Step 1: PG 승인
Orchestrator->>PG: 승인 요청
PG-->>Orchestrator: 승인 성공
Orchestrator->>Orchestrator: 상태: PG_APPROVED 저장
Note over Orchestrator: Step 2: Order 생성
Orchestrator->>Order: Order 생성 요청
Order-->>Orchestrator: 생성 실패 ❌
Note over Orchestrator: 보상 트랜잭션 실행
Orchestrator->>PG: PG 취소 요청
PG-->>Orchestrator: 취소 성공 ✅
Orchestrator->>Orchestrator: 상태: CREATE_ORDER_FAILED 저장
Orchestrator-->>Client: 결제 실패 응답
두 가지 구현 방식
1. Choreography (오케스트레이션 없음)
각 서비스가 이벤트를 발행하고, 다른 서비스가 이를 구독해서 다음 단계를 수행합니다.
Payment Service → [PG 승인 완료 이벤트 발행]
Order Service → [이벤트 구독] → Order 생성
장점:
- 중앙 집중식 오케스트레이터가 필요 없습니다.
- 서비스 간 결합이 느슨합니다.
단점:
- 전체 흐름을 파악하기 어렵습니다.
- 디버깅이 복잡합니다.
- 보상 트랜잭션 관리가 어렵습니다.
2. Orchestration (오케스트레이션)
중앙 오케스트레이터가 전체 흐름을 관리하고 각 서비스를 순차적으로 호출합니다.
Orchestrator
→ Step 1: PG 승인
→ Step 2: Order 생성
→ Step 3: 완료 처리
(실패 시 보상 트랜잭션 실행)
장점:
- 전체 흐름이 명확합니다.
- 보상 트랜잭션 관리가 쉽습니다.
- 디버깅이 용이합니다.
단점:
- 중앙 집중식 오케스트레이터가 필요합니다.
- 오케스트레이터가 단일 장애점이 될 수 있습니다.
우리 프로젝트는 Orchestration 방식을 선택했습니다. 이유는 보상 트랜잭션 관리가 더 명확하고, 디버깅이 쉬워서입니다.
3. 실제 프로젝트 적용: Payment Service
아키텍처 개요
graph TB
subgraph "Payment Service (Orchestrator)"
PS[ProductPaymentService]
end
subgraph "External Services"
PG[NicePay API<br/>PG Service]
OS[Product Service<br/>Order Service]
end
PS -->|Step 1: 승인 요청| PG
PG -->|승인 성공| PS
PS -->|Step 2: Order 생성| OS
OS -->|생성 성공/실패| PS
PS -.->|실패 시<br/>보상 트랜잭션| PG
style PS fill:#fff4e1
style PG fill:#ffe1f5
style OS fill:#e1f5ff
흐름:
- Step 1: PG 승인 (NicePay API)
- Step 2: Order 생성 (Product Service)
- Step 3: 결제 완료 처리
- 실패 시: 보상 트랜잭션 실행
상태 머신 설계
결제는 여러 상태를 거치며, 각 상태 전이는 엄격하게 제어됩니다:
enum class ProductPaymentStatus {
PENDING, // 초기 상태
PG_APPROVED, // PG 승인 완료
COMPLETED, // 최종 완료
PG_APPROVE_FAILED, // PG 승인 실패
CREATE_ORDER_FAILED, // Order 생성 실패
COMPENSATION_NEEDED, // 보상 필요 (수동 처리)
CANCELLED, // 취소됨
PG_CANCEL_FAILED, // PG 취소 실패
ORDER_CANCEL_FAILED, // Order 취소 실패
}
상태 전이 규칙:
PENDING → PG_APPROVED → COMPLETED (성공 경로)
PENDING → PG_APPROVE_FAILED (PG 승인 실패)
PG_APPROVED → CREATE_ORDER_FAILED (Order 생성 실패)
PG_APPROVED → COMPENSATION_NEEDED (보상 실패)
도메인 모델: 상태 전이 메서드
도메인 모델에 상태 전이 로직을 넣어서 불변성을 보장합니다:
data class ProductPayment(
val id: String,
val userId: String,
val amount: Int,
val status: ProductPaymentStatus,
val pgTid: String? = null,
val referenceId: String? = null,
// ...
) {
fun canApprove(): Boolean {
return status == ProductPaymentStatus.PENDING
}
fun markPgApproved(tid: String?): ProductPayment {
require(status == ProductPaymentStatus.PENDING) {
"PG 승인 상태로 변경할 수 없습니다: $status"
}
return this.copy(
status = ProductPaymentStatus.PG_APPROVED,
pgTid = tid,
updatedAt = Instant.now(),
)
}
fun markAsCompleted(referenceId: String?): ProductPayment {
require(status == ProductPaymentStatus.PG_APPROVED) {
"완료 상태로 변경할 수 없습니다: $status"
}
return this.copy(
status = ProductPaymentStatus.COMPLETED,
referenceId = referenceId,
updatedAt = Instant.now(),
)
}
}
핵심 포인트:
- 상태 전이는 도메인 모델 내부에서 검증됩니다.
- 불변 객체로 상태를 변경합니다 (copy 사용).
- 잘못된 상태 전이는 예외를 발생시킵니다.
4. Saga 패턴 구현: 결제 승인 프로세스
전체 흐름
@Service
@Transactional
class ProductPaymentService(
private val paymentRepository: PaymentRepository,
private val productServiceClient: ProductServiceClient,
private val nicePayService: NicePayService,
private val productPaymentStatusService: ProductPaymentStatusService,
private val notificationService: NotificationService,
) : ProductPaymentUseCase {
override fun approvePayment(
orderId: String,
tid: String?,
authToken: String?,
amount: String?,
): ProductPayment {
val payment = paymentRepository.findProductPaymentByPgOrderId(orderId)
?: throw IllegalArgumentException("결제를 찾을 수 없습니다: $orderId")
require(payment.canApprove()) {
"이미 처리된 결제입니다: ${payment.status}"
}
var currentPayment = payment
try {
// Step 1: PG 승인
if (payment.pgType == PgType.NICEPAY && tid != null) {
nicePayService.approvePayment(
tid = tid,
amount = payment.amount,
)
logger.info("PG 승인 완료: paymentId=${payment.id}")
// 중간 상태 저장 (별도 트랜잭션)
currentPayment = productPaymentStatusService.markAsPgApproved(payment, tid)
}
// Step 2: Order 생성
val order = productServiceClient.createOrder(
productType = currentPayment.productType,
productId = currentPayment.productId,
userId = currentPayment.userId,
paymentId = currentPayment.id,
)
logger.info("Order 생성 완료: paymentId=${currentPayment.id}, orderId=${order.id}")
// Step 3: 최종 완료
val updated = currentPayment.markAsCompleted(order.id)
val savedPayment = paymentRepository.saveProductPayment(updated)
notificationService.sendPaymentSuccessNotification(savedPayment)
return savedPayment
} catch (e: Exception) {
// Saga 패턴: 상태별 실패 처리 및 보상 트랜잭션
handleFailure(currentPayment, e, tid)
throw e
}
}
}
핵심: 실패 처리 및 보상 트랜잭션
private fun handleFailure(
currentPayment: ProductPayment,
e: Exception,
tid: String?,
) {
logger.error("결제 승인 실패: paymentId=${currentPayment.id}, error=${e.message}", e)
notificationService.sendPaymentFailureNotification(currentPayment, e)
// Saga 패턴: 상태별 실패 처리 및 보상 트랜잭션
when (currentPayment.status) {
ProductPaymentStatus.PENDING -> {
// Step 1 실패: PG 승인 실패
// 보상 트랜잭션 불필요 (아직 PG 승인 전)
productPaymentStatusService.markAsPgApproveFailed(currentPayment, tid)
}
ProductPaymentStatus.PG_APPROVED -> {
// Step 2 실패: Order 생성 실패 (PG는 이미 성공)
// 보상 트랜잭션 필요: PG 취소
try {
compensatePgApproval(currentPayment, tid)
productPaymentStatusService.markAsCreateOrderFailed(currentPayment)
} catch (compensationException: Exception) {
logger.error(
"보상 트랜잭션 실패: paymentId=${currentPayment.id}, " +
"error=${compensationException.message}",
compensationException,
)
// 보상 트랜잭션도 실패 → 수동 처리 필요
productPaymentStatusService.markAsCompensationNeeded(
currentPayment,
e.message,
compensationException.message,
)
}
}
else -> {
logger.warn(
"예상치 못한 상태에서 실패: paymentId=${currentPayment.id}, " +
"status=${currentPayment.status}",
)
}
}
}
보상 트랜잭션 구현
/**
* 보상 트랜잭션: PG 승인 취소
*/
private fun compensatePgApproval(payment: ProductPayment, tid: String?) {
if (tid != null && payment.pgType == PgType.NICEPAY) {
logger.info("PG 승인 취소 시작: paymentId=${payment.id}")
try {
nicePayService.cancelPayment(
tid = tid,
amount = payment.amount,
reason = "Order 생성 실패로 인한 자동 취소",
)
logger.info("PG 승인 취소 완료: paymentId=${payment.id}")
} catch (e: Exception) {
logger.error("PG 승인 취소 실패: paymentId=${payment.id}, error=${e.message}", e)
throw e // 보상 트랜잭션 실패는 상위로 전파
}
}
}
5. 중간 상태 저장: 별도 트랜잭션
왜 중간 상태를 저장해야 하나?
각 단계가 성공하면 즉시 상태를 저장해야 합니다. 그래야 실패했을 때 어느 단계에서 실패했는지 알 수 있고, 보상 트랜잭션을 올바르게 실행할 수 있습니다.
Propagation.REQUIRES_NEW 사용
abstract class BasePaymentStatusService<T : Payment>(
protected val paymentRepository: PaymentRepository,
) {
@Transactional(propagation = Propagation.REQUIRES_NEW)
protected open fun markStatus(
payment: T,
statusName: String,
changeStatus: (T) -> T,
): T {
logger.info("$statusName 상태로 저장: paymentId=${payment.id}")
val updated = changeStatus(payment)
return savePayment(updated)
}
protected abstract fun savePayment(payment: T): T
}
REQUIRES_NEW를 사용하는 이유:
- 외부 서비스 호출 전에 현재 상태를 저장합니다.
- 외부 서비스 호출이 실패해도 이미 저장된 상태는 유지됩니다.
- 보상 트랜잭션 실행 시 올바른 상태 정보를 사용할 수 있습니다.
흐름:
1. PG 승인 API 호출
2. 성공 → markAsPgApproved() (REQUIRES_NEW 트랜잭션)
→ DB에 PG_APPROVED 상태 저장
3. Order 생성 API 호출
4. 실패 → 보상 트랜잭션 실행
→ 저장된 PG_APPROVED 상태를 기반으로 PG 취소
6. 다양한 결제 타입에 대한 확장
전략 패턴 활용
프로젝트에는 세 가지 결제 타입이 있습니다:
- ProductPayment: 상품 결제 (Order 생성)
- CreditPayment: 크레딧 충전 (Credit 발급)
- QuotaPayment: 할당량 구매 (Quota 발급)
각 타입마다 Step 2가 다르지만, 전체 흐름은 동일합니다:
// ProductPayment
Step 1: PG 승인
Step 2: Order 생성 (Product Service)
Step 3: 완료
// CreditPayment
Step 1: PG 승인
Step 2: Credit 발급 (Supporters Service)
Step 3: 완료
// QuotaPayment
Step 1: PG 승인
Step 2: Quota 발급 (Book Plus Service)
Step 3: 완료
공통 추상화
abstract class BasePaymentStatusService<T : Payment> {
@Transactional(propagation = Propagation.REQUIRES_NEW)
protected open fun markStatus(...) { ... }
}
// 각 타입별 구현
class ProductPaymentStatusService : BasePaymentStatusService<ProductPayment>
class CreditPaymentStatusService : BasePaymentStatusService<CreditPayment>
class QuotaPaymentStatusService : BasePaymentStatusService<QuotaPayment>
7. 실패 시나리오별 처리
시나리오 1: PG 승인 실패
Step 1: PG 승인 → 실패
처리:
- 보상 트랜잭션이 필요 없습니다 (아직 외부 서비스 호출 전).
- 상태를
PG_APPROVE_FAILED로 변경합니다.
ProductPaymentStatus.PENDING -> {
productPaymentStatusService.markAsPgApproveFailed(currentPayment, tid)
}
시나리오 2: Order 생성 실패 (PG는 성공)
Step 1: PG 승인 → 성공 ✅
Step 2: Order 생성 → 실패 ❌
처리:
- 보상 트랜잭션을 실행합니다: PG 취소.
- 상태를
CREATE_ORDER_FAILED로 변경합니다.
ProductPaymentStatus.PG_APPROVED -> {
try {
compensatePgApproval(currentPayment, tid) // PG 취소
productPaymentStatusService.markAsCreateOrderFailed(currentPayment)
} catch (compensationException: Exception) {
// 보상 트랜잭션도 실패
productPaymentStatusService.markAsCompensationNeeded(...)
}
}
시나리오 3: 보상 트랜잭션 실패
Step 1: PG 승인 → 성공 ✅
Step 2: Order 생성 → 실패 ❌
보상: PG 취소 → 실패 ❌
처리:
- 상태를
COMPENSATION_NEEDED로 변경합니다. - 수동 처리가 필요합니다 (관리자 알림).
- 에러 정보를 metadata에 저장합니다.
productPaymentStatusService.markAsCompensationNeeded(
currentPayment,
e.message, // 원본 에러
compensationException.message, // 보상 에러
)
8. 취소 프로세스
취소도 Saga 패턴
결제 취소도 여러 단계를 거치므로 Saga 패턴을 적용합니다:
override fun cancelPayment(paymentId: String): ProductPayment {
val payment = paymentRepository.findProductPaymentById(paymentId)
?: throw IllegalArgumentException("결제를 찾을 수 없습니다: $paymentId")
require(payment.canCancel()) { "취소할 수 없는 상태입니다: ${payment.status}" }
// Step 1: PG 취소
if (payment.pgType == PgType.NICEPAY && payment.pgTid != null) {
try {
nicePayService.cancelPayment(
tid = payment.pgTid,
amount = payment.amount,
reason = "결제 취소",
)
logger.info("PG 취소 완료: paymentId=${payment.id}")
} catch (e: Exception) {
logger.error("PG 취소 실패: paymentId=${payment.id}, error=${e.message}", e)
productPaymentStatusService.markAsPgCancelFailed(payment, e.message)
throw IllegalStateException("PG 취소 실패로 인해 취소를 진행할 수 없습니다.", e)
}
}
// Step 2: 주문 취소
if (payment.status == ProductPaymentStatus.COMPLETED && payment.referenceId != null) {
try {
productServiceClient.cancelOrder(
productType = payment.productType,
productId = payment.productId,
userId = payment.userId,
paymentId = payment.id,
)
logger.info("주문 취소 완료: paymentId=${payment.id}")
} catch (e: Exception) {
logger.error("주문 취소 실패: paymentId=${payment.id}, error=${e.message}", e)
productPaymentStatusService.markAsOrderCancelFailed(payment, e.message)
throw IllegalStateException(
"PG 취소는 완료되었으나 주문 취소 실패. 수동 처리 필요: paymentId=${payment.id}",
e,
)
}
}
// Step 3: 최종 취소 완료
val cancelled = payment.cancel()
return paymentRepository.saveProductPayment(cancelled)
}
9. 모니터링 및 알림
실패 알림
각 실패 시나리오에서 알림을 발송합니다:
catch (e: Exception) {
logger.error("결제 승인 실패: paymentId=${payment.id}, error=${e.message}", e)
// 실패 알림 발송
notificationService.sendPaymentFailureNotification(payment, e)
// 보상 트랜잭션 처리
handleFailure(currentPayment, e, tid)
throw e
}
수동 처리 필요 상태
COMPENSATION_NEEDED 상태는 수동 처리가 필요하므로:
- 관리자 대시보드에 표시합니다.
- 주기적으로 재시도 스케줄러를 실행합니다.
- 알림을 발송합니다.
10. Saga 패턴의 장단점
장점
분산 트랜잭션 처리 가능
- 여러 서비스에 걸친 트랜잭션을 처리할 수 있습니다.
- 각 서비스의 독립성을 유지합니다.
부분 실패 처리
- 어느 단계에서 실패했는지 명확히 알 수 있습니다.
- 보상 트랜잭션으로 일관성을 유지합니다.
확장성
- 새로운 단계 추가가 쉽습니다.
- 각 서비스가 독립적으로 발전할 수 있습니다.
가용성
- 일부 서비스가 실패해도 다른 서비스는 정상 동작합니다.
- 전체 시스템이 다운되지 않습니다.
단점
복잡성 증가
- 보상 트랜잭션 로직이 필요합니다.
- 상태 관리가 복잡합니다.
일관성 지연 (Eventual Consistency)
- 모든 단계가 완료될 때까지 일관성이 보장되지 않습니다.
- 중간 상태에서 조회 시 불일치가 가능합니다.
보상 트랜잭션 실패 처리
- 보상 트랜잭션도 실패할 수 있습니다.
- 수동 처리 필요 상황이 발생할 수 있습니다.
11. 주의사항 및 베스트 프랙티스
1. 멱등성 보장
각 단계와 보상 트랜잭션은 멱등성(idempotency)을 보장해야 합니다:
// ✅ 멱등성 보장
fun approvePayment(orderId: String, ...): ProductPayment {
val payment = paymentRepository.findProductPaymentByPgOrderId(orderId)
// 이미 처리된 경우 재처리하지 않음
if (payment.status == ProductPaymentStatus.COMPLETED) {
return payment
}
// ...
}
2. 타임아웃 처리
외부 서비스 호출 시 타임아웃을 설정합니다:
@FeignClient(name = "product-service")
interface ProductServiceClient {
@PostMapping("/orders")
fun createOrder(@RequestBody request: CreateOrderRequest): OrderResponse
// 타임아웃은 Feign 설정에서 관리
}
3. 재시도 전략
일시적 실패에 대한 재시도:
@Retryable(
value = [RetryableException::class],
maxAttempts = 3,
backoff = Backoff(delay = 1000, multiplier = 2)
)
fun createOrder(...): OrderResponse {
// ...
}
4. 상태 저장
각 단계의 결과를 저장해서 추적 가능하게 합니다:
data class ProductPayment(
val status: ProductPaymentStatus,
val pgTid: String?, // PG 승인 결과
val referenceId: String?, // Order ID
val metadata: Map<String, String>, // 추가 정보
// ...
)
5. 로깅
상세한 로깅으로 디버깅을 쉽게 합니다:
logger.info("PG 승인 완료: paymentId=${payment.id}")
logger.error("결제 승인 실패: paymentId=${payment.id}, error=${e.message}", e)
logger.warn("보상 트랜잭션 실패: paymentId=${payment.id}")
12. 실제 운영 경험
발생한 문제들
문제 1: 보상 트랜잭션 실패
상황:
- PG 승인 성공
- Order 생성 실패
- PG 취소 API 호출 실패 (네트워크 오류)
해결:
COMPENSATION_NEEDED상태로 저장했습니다.- 관리자 대시보드에 알림을 보냈습니다.
- 수동으로 PG 취소를 처리했습니다.
- 향후 자동 재시도 스케줄러를 추가할 예정입니다.
문제 2: 중복 처리
상황:
- 클라이언트가 타임아웃 후 재시도
- 이미 처리 중인 결제가 중복 처리됨
해결:
- 상태 체크로 중복 처리를 방지했습니다.
- 멱등성을 보장했습니다.
개선 사항
-
자동 재시도 스케줄러
COMPENSATION_NEEDED상태의 결제를 주기적으로 재시도합니다.
-
상태 모니터링
- 각 상태별 결제 수를 모니터링합니다.
- 이상 상태 알림을 설정합니다.
-
보상 트랜잭션 강화
- 보상 트랜잭션에도 재시도 로직을 추가합니다.
- 지수 백오프를 적용합니다.
13. Next Actions: 개선할 수 있는 부분
현재 구현은 Saga 패턴의 핵심을 잘 구현하고 있지만, 다음과 같은 개선 사항을 고려할 수 있습니다:
1. 자동 재시도 스케줄러 구현
COMPENSATION_NEEDED 상태의 결제를 주기적으로 재시도하는 스케줄러를 추가하여 수동 처리 부담을 줄일 수 있습니다.
2. 보상 트랜잭션 재시도 로직 강화
지수 백오프를 사용한 재시도 메커니즘을 추가하여 일시적 네트워크 오류를 자동으로 복구할 수 있습니다.
3. 모니터링 및 알림 강화
Prometheus + Grafana로 각 상태별 결제 수를 모니터링하고, 이상 상태를 조기 감지할 수 있습니다.
4. 멱등성 키 강화
명시적인 멱등성 키를 도입하여 네트워크 재시도 시 중복 처리를 완전히 방지할 수 있습니다.
5. 분산 추적 강화
Zipkin 또는 Jaeger를 도입하여 전체 Saga 흐름을 시각화하고 디버깅 시간을 단축할 수 있습니다.
6. Saga 오케스트레이터 프레임워크 도입 (장기적)
Temporal이나 Eventuate 같은 프레임워크를 도입하여 Saga 로직을 중앙화하고 자동 재시도 및 보상 트랜잭션을 관리할 수 있습니다.
결론
Saga 패턴은 마이크로서비스 환경에서 분산 트랜잭션을 처리하는 강력한 패턴입니다.
핵심 포인트:
- 긴 트랜잭션을 작은 트랜잭션으로 분할
- 각 단계의 상태를 명확히 저장
- 실패 시 보상 트랜잭션 실행
- 보상 트랜잭션 실패 시 수동 처리 프로세스
적용 시 고려사항:
- 상태 머신 설계
- 보상 트랜잭션 구현
- 멱등성 보장
- 모니터링 및 알림
- 수동 처리 프로세스
최종 코드 구조:
// Orchestrator
@Service
class ProductPaymentService {
fun approvePayment(...) {
try {
step1() // PG 승인
step2() // Order 생성
step3() // 완료
} catch (e: Exception) {
handleFailure() // 보상 트랜잭션
}
}
}
// 상태 관리
abstract class BasePaymentStatusService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun markStatus(...) { ... }
}
// 도메인 모델
data class ProductPayment {
fun markPgApproved(...) { ... }
fun markAsCompleted(...) { ... }
}
Saga 패턴을 올바르게 적용하면 분산 환경에서도 안정적인 트랜잭션 처리가 가능합니다.