들어가며: 멀티스텝 워크플로우의 아킬레스건, 부분 실패
분산 시스템에서 여러 단계를 거치는 작업을 처리할 때, 모든 단계가 성공한다면 문제가 없습니다. 하지만 현실은 그렇지 않죠. 2단계에서 실패하면 이미 완료된 1단계의 결과는 어떻게 처리할까요? 단순히 '실패'라고 표시하고 끝내기에는 이미 외부 시스템에 영향을 준 상태일 수 있습니다.
예를 들어, 은행 계좌 이체 워크플로우를 떠올려 봅시다.
- A은행에서 출금
- B은행에 입금
- 두 계좌 소유주에게 이메일 발송
2단계가 실패하면, 1단계에서 출금된 금액은 어떻게 해야 할까요? A은행 시스템에서 해당 거래를 직접 '실행 취소'할 수는 없습니다. 새로운 역연산(반대 방향의 거래)을 통해 원래 상태로 되돌려야 합니다. 즉, A은행에 다시 입금하는 보상(Compensation) 작업이 필요합니다.
이렇게 정방향 작업과 그에 대응하는 보상 로직을 하나의 쌍으로 묶어 관리하는 패턴을 바로 Saga 패턴이라고 합니다. 오늘은 Cloudflare Workflows에 새롭게 추가된 이 Saga 패턴 공식 지원 기능을 깊이 있게 파헤쳐 보겠습니다.

본론 1: 기존 방식의 문제점과 새로운 rollback 옵션
기존 방식: 수동 보상 로직의 늪
이전에는 개발자가 직접 보상 로직을 구현해야 했습니다. 실패 시 어떤 단계가 성공했는지 추적하고, 역순으로 보상 작업을 실행하는 복잡한 코드를 작성해야 했죠.
// 기존 방식: 수동으로 성공한 단계를 추적하고 역순으로 보상
let debitA;
let creditB;
try {
debitA = await step.do("debit-bank-a", () => bankA.debit(from, amount));
creditB = await step.do("credit-bank-b", () => bankB.credit(to, amount));
await step.do("notify", () => notifyBoth(from, to, amount));
} catch (error) {
// 역순으로 보상 실행. 각 보상은 독립적인 durable step이어야 하며,
// 멱등성을 가져야 하고, 하나가 실패해도 계속 진행되어야 함.
if (creditB) {
try {
await step.do("reverse-credit-b", () => bankB.debit(to, amount, creditB.id));
} catch (e) {
await alertOnCall("reverse-credit-b failed", e);
}
}
if (debitA) {
try {
await step.do("refund-debit-a", () => bankA.credit(from, amount, debitA.id));
} catch (e) {
await alertOnCall("refund-debit-a failed", e);
}
}
throw error;
}
이 방식은 몇 가지 큰 문제점을 가지고 있습니다.
- 코드 복잡도 증가: 성공한 단계를 추적하는 변수와 분기 처리 로직이 늘어납니다.
- 로직 분산: 각 단계의 정의와 보상 로직이 분리되어 있어, 워크플로우의 전체적인 흐름을 파악하기 어렵습니다.
- 실수 가능성: 새 단계를 추가할 때 보상 로직을 빼먹기 쉽습니다.
새로운 방식: step.do()에 rollback 옵션 추가
Cloudflare Workflows는 이제 step.do() 함수에 rollback 옵션을 제공하여 이 문제를 해결합니다. 각 단계의 정의 안에 보상 로직을 함께 선언할 수 있게 된 것입니다.
// 새로운 방식: 각 단계에 보상 로직을 함께 정의
await step.do("debit-bank-a", () => bankA.debit(from, amount), {
rollback: async ({ output }) => bankA.credit(from, amount, output.id),
});
await step.do("credit-bank-b", () => bankB.credit(to, amount), {
rollback: async ({ output }) => bankB.debit(to, amount, output.id),
});
await step.do("notify", () => notifyBoth(from, to, amount));
이렇게 하면 각 단계의 정방향 작업과 역방향 보상 작업이 하나의 단위로 묶입니다. 새 단계를 추가할 때 해당 단계의 롤백 로직도 함께 추가하면 되므로, 누락 위험이 줄어듭니다. 또한, 단계가 실패하면 Cloudflare Workflows가 자동으로 역순으로 보상 핸들러를 실행합니다.
실제 사용 예시: 멱등성 보장하기
보상 함수를 작성할 때 가장 중요한 것은 멱등성(Idempotency) 입니다. 네트워크 오류 등으로 인해 보상 함수가 여러 번 호출될 수 있기 때문에, 같은 요청을 여러 번 보내도 동일한 결과를 보장해야 합니다. 결제 게이트웨이나 재고 시스템에서 제공하는 Idempotency-Key를 활용하는 것이 일반적입니다.
const transferId = "transfer-12345"; // 워크플로우 실행 ID
// 출금 단계
const debit = await step.do(
"debit-account-a",
async () => {
return await bankA.debit({
accountId: fromAccountId,
amount,
idempotencyKey: `${transferId}:debit-account-a`,
});
},
{
rollback: async () => {
await bankA.credit({
accountId: fromAccountId,
amount,
idempotencyKey: `${transferId}:rollback-debit-account-a`,
});
},
}
);
// 입금 단계
const credit = await step.do(
"credit-account-b",
async () => {
return await bankB.credit({
accountId: toAccountId,
amount,
idempotencyKey: `${transferId}:credit-account-b`,
});
},
{
rollback: async ({ output }) => {
// 단계가 실패하여 output이 undefined일 수 있으므로 반드시 처리
if (output === undefined) {
return;
}
await bankB.debit({
accountId: toAccountId,
amount,
idempotencyKey: `${transferId}:rollback-credit-account-b`,
});
},
}
);
// 알림 단계 (실패 시 이전 단계 롤백)
await step.do("send-confirmation", async () => {
await sendTransferConfirmation({ ... });
});
코드 설명:
idempotencyKey: 각 정방향 및 역방향 작업에 고유한 키를 부여하여, 재시도 시 중복 실행을 방지합니다.transferId에 단계 이름을 조합해 사용했습니다.output인자:rollback함수는{ output }객체를 인자로 받습니다.output은 정방향 단계가 성공적으로 반환한 값입니다. 만약 단계가 실패하여output이undefined일 수 있으므로, 이에 대한 처리를 필수로 해야 합니다.

본론 2: 설계 철학, 주의사항, 그리고 심화 팁
왜 rollback 옵션인가? (API 설계 철학)
Cloudflare 팀은 롤백 기능을 추가하면서 여러 가지 API 디자인을 고려했다고 합니다.
- Fluent API (
step.do().rollback()): 체이닝이 자연스러워 보이지만,step.do()가 반환하는 Promise에.rollback()메서드를 추가하는 것은 Promise 파이프라이닝(Promise Pipelining)과 같은 Workers RPC의 중요한 기능과 충돌할 수 있습니다. Promise가 실제 결과값 대신 미래의 결과를 가리키는 '핸들'로 사용되는 상황에서,.rollback()이 Promise의 메서드인지, 아니면 다른 의미인지 모호해집니다. 또한,step.do()가 호출되는 시점과 Promise가 실제로 소비되는 시점이 달라질 수 있어, 단계의 시작 타이밍을 예측하기 어려워집니다. - Builder API (
step.saga().do().rollback().run()): 명확하지만, 모든 단계에.run()을 붙여야 하는 번거로움이 있습니다. 또한,step.do()를 주력 API로 유지하려는 철학과도 맞지 않습니다.
결국, step.do(..., { rollback }) 형태의 메타데이터 방식을 선택했습니다. 이는 기존 step.do()의 동작을 변경하지 않으면서, 각 단계의 수명주기(Lifecycle)에 롤백이라는 새로운 동작을 추가하는 가장 자연스러운 방법이기 때문입니다.
이 기술의 한계 또는 주의사항 (비판적 시각)
- 롤백은 '최선의 노력(Best Effort)'입니다. 롤백 핸들러 자체가 실패할 수 있으며, 설정된 재시도 횟수를 모두 소진하면 워크플로우는
Errored상태로 종료됩니다. 롤백이 항상 성공한다고 보장할 수는 없습니다. output === undefined처리: 실패한 단계의 롤백 핸들러는output으로undefined를 받을 수 있습니다. 이는 단계가 외부 시스템과 상호작용한 후, 그 결과값을 Workflows에 반환하기 전에 실패했을 수 있기 때문입니다. 따라서, 롤백 핸들러는output이undefined인 경우에도 안전하게 동작하도록 설계해야 합니다.- 복잡한 트랜잭션에는 부적합: Saga 패턴은 각 단계가 독립적인 트랜잭션을 가지는 분산 환경에 적합합니다. 만약 단일 데이터베이스 내에서 강한 일관성이 요구되는 트랜잭션이라면, 기존의 ACID 트랜잭션을 사용하는 것이 더 적합할 수 있습니다.
한국 개발 생태계에서의 적용 맥락
국내 핀테크, 이커머스, MSA 환경에서도 다중 서비스에 걸친 워크플로우를 구현할 때 Saga 패턴은 매우 유용합니다. 예를 들어, 쿠팡이나 배달의민족 같은 서비스에서 주문 -> 결제 -> 재고 차감 -> 배송 시작으로 이어지는 프로세스를 관리할 때, 특정 단계가 실패하면 이전 단계를 보상해야 하는 상황이 발생합니다. 이때 Cloudflare Workflows의 롤백 기능을 사용하면, 별도의 상태 관리 서버나 복잡한 보상 로직을 구현할 필요 없이 각 단계에 롤백 핸들러만 정의해 주면 됩니다. 특히, AWS Step Functions나 Azure Durable Functions를 사용 중이라면, 이와 유사한 기능을 Cloudflare의 서버리스 환경에서도 사용할 수 있다는 점을 참고하시면 좋습니다.
다음 단계 학습 방향 제시
- 멱등성(Idempotency) 마스터하기: 롤백 로직의 핵심은 멱등성입니다. 다양한 외부 API(결제, 이메일, 재고)에서 멱등성을 보장하는 방법을 학습하세요.
- Saga 패턴의 다양한 변형: Saga 패턴에는
Choreography(이벤트 기반)와Orchestration(중앙 집중식) 두 가지 주요 구현 방식이 있습니다. Cloudflare Workflows는 Orchestration 방식에 가깝습니다. 각 방식의 장단점을 비교해 보세요. - 워크플로우 엔진 비교: AWS Step Functions, Temporal, Camunda 등 다른 워크플로우 엔진과 Cloudflare Workflows의 차이점을 비교 분석해 보세요. 특히, 내구성(Durability)을 보장하는 방식과 실행 모델의 차이를 이해하는 것이 중요합니다.

결론: 실무 적용 조언 및 마무리
Cloudflare Workflows의 Saga 롤백 기능은 분산 트랜잭션 처리의 복잡성을 상당 부분 추상화해 줍니다. step.do()에 rollback 옵션을 추가하는 것만으로도, 기존에 수동으로 구현하던 많은 보상 로직을 대체할 수 있습니다.
실무 적용 시 꼭 기억해야 할 점:
- 롤백 핸들러는 반드시 멱등성을 가지도록 설계하세요.
output이undefined일 수 있는 상황을 항상 고려하세요.- 롤백 자체도 워크플로우의 일부이므로, 재시도 및 타임아웃 설정을 적절히 구성하세요.
이 기능은 '실패'를 단순히 오류로 처리하는 것이 아니라, 시스템을 일관된 상태로 되돌리기 위한 전략을 설계할 수 있게 해줍니다. 다중 단계 애플리케이션을 구축하고 있다면, 이번 기회에 Saga 패턴을 도입해 보는 것은 어떨까요?
함께 보면 좋은 글:
- Google Colab CLI 공개 로컬 터미널에서 A100 GPU로 ML 워크플로우를 실행하는 법
- React Server Components 치명적 보안 취약점(CVE-2025-55182) 지금 당장 확인해야 할 사항
참고 자료: Cloudflare 블로그 원문