본문으로 건너뛰기

Temporal로 보상(Saga) 트랜잭션 다루기

· 약 5분

여러 서비스에 걸친 작업을 하나의 트랜잭션으로 묶을 수 없을 때, 보상(Saga) 패턴과 Temporal 워크플로우로 안전하게 되돌리는 방법을 정리합니다.

결론부터 말하면, 여러 서비스에 걸친 작업은 단계마다 "되돌리기(보상)"를 짝지어 두고, 실패하면 성공한 단계를 역순으로 취소합니다. 이 흐름을 손으로 추적하지 말고 워크플로우 엔진(Temporal)에 맡깁니다.

분산 트랜잭션이 왜 어려운가

분산 트랜잭션이 어려운 이유는, 여러 서비스(또는 외부 시스템)에 걸친 작업을 하나의 DB 트랜잭션으로 묶을 수 없기 때문입니다. 한 DB 안의 작업은 COMMIT/ROLLBACK으로 전부 성공 또는 전부 취소가 되지만, 결제 서비스와 재고 서비스가 따로 있으면 그런 공통 롤백이 없습니다.

예를 들어 "결제 → 재고 차감 → 배송 등록"에서 결제는 됐는데 재고 차감이 실패하면, 이미 된 결제를 따로 되돌려(환불) 줘야 합니다. 자동 롤백이 없으니 직접 되돌려야 합니다.

한 DB 트랜잭션과 달리 여러 서비스에 걸친 작업은 공통 롤백이 없어 보상으로 직접 되돌려야 한다

Saga 패턴이란

Saga 패턴은 여러 단계로 이루어진 작업을, 각 단계와 그 단계를 되돌리는 보상(compensation) 동작을 짝지어 관리하는 방식입니다. 전체를 한 번에 롤백하는 대신, 성공한 단계만 거꾸로 취소합니다.

호텔·항공·렌터카를 묶어 예약하는 상황에 비유할 수 있습니다. 렌터카 예약에서 실패하면, 이미 잡아 둔 항공과 호텔 예약을 취소해야 합니다. 이 "취소"가 보상 동작입니다.

보상은 역순으로

여러 단계 작업 중 실패 시 보상 단계를 역순으로 실행하는 Saga 패턴

보상은 성공한 단계의 역순으로 실행합니다. 마지막에 성공한 단계부터 거꾸로 되돌려야, 단계 사이의 의존 관계가 깨지지 않기 때문입니다.

정방향:  결제  →  재고 차감  →  배송 등록(실패)
보상: 재고 복구 ← 결제 환불 (역순으로 취소)

Temporal로 구현하기

Temporal에서는 성공한 단계의 보상 동작을 리스트에 쌓아 두고, 실패 시 역순으로 실행합니다.

from temporalio import workflow

@workflow.defn
class OrderWorkflow:
@workflow.run
async def run(self, order) -> None:
compensations = []
try:
await workflow.execute_activity(charge_payment, order, ...)
compensations.append(refund_payment) # 결제의 보상

await workflow.execute_activity(reserve_inventory, order, ...)
compensations.append(release_inventory) # 재고의 보상

await workflow.execute_activity(register_shipping, order, ...)
except Exception:
# 성공한 단계만 역순으로 되돌림
for compensate in reversed(compensations):
await workflow.execute_activity(compensate, order, ...)
raise

핵심은 두 가지입니다. 한 단계가 성공한 직후에 그 보상을 등록하고, 실패하면 쌓인 보상을 역순으로 실행합니다. "어디까지 진행됐는지"는 Temporal이 기억하므로, 서버가 중간에 죽어도 멈춘 지점부터 이어집니다.

흔한 함정

  • 보상 단계를 빠뜨림: 정방향만 짜고 보상을 안 만들면, 실패 시 결제만 되고 재고는 안 잡힌 상태가 남습니다. 단계마다 보상을 짝으로 둡니다.
  • 보상에 멱등성이 없음: 보상이 두 번 실행될 수 있습니다(재시도). "이미 환불됨"이면 또 환불하지 않도록 멱등하게 만듭니다.
  • 보상도 실패할 수 있음: 환불 API가 죽을 수도 있습니다. 보상 활동(activity)에도 재시도 정책을 두고, 끝내 실패하면 사람이 처리하도록 알립니다.

Q&A

  • Saga와 2단계 커밋(2PC)은 무엇이 다른가요?
    • 2PC는 모든 참가자를 잠그고 한 번에 커밋합니다. Saga는 잠그지 않고 단계별로 진행한 뒤, 실패 시 보상으로 되돌립니다. 서비스가 분리된 환경에서는 보통 Saga가 현실적입니다.
  • 꼭 Temporal이어야 하나요?
    • 아니요. 메시지 큐로도 Saga를 구현할 수 있습니다. 다만 "어디까지 진행됐나"와 재시도·타임아웃을 직접 관리해야 합니다. Temporal은 그 상태 관리를 대신 해 줍니다.
  • 보상 중에 또 실패하면요?
    • 보상도 재시도 대상입니다. 자동 재시도로도 안 되면 알림을 띄워 수동 개입으로 넘깁니다.

참고자료

  • Temporal Docs: Workflows / Activities / Saga
  • Microservices.io: Saga pattern