Article@wishee0·2026년 6월 25일·0

How we built saga rollbacks for Cloudflare Workflows

Quick Summary

Cloudflare Workflows는 다단계 작업 실패 시 각 단계 안에 선언한 보상 로직을 역순으로 실행하는 saga rollback을 도입해, 내구성 있는 워크플로에서 부분 완료 상태를 더 명확하게 되돌릴 수 있게 했다.

@wishee0blog.cloudflare.com원문 보기
How we built saga rollbacks for Cloudflare Workflows 관련 대표 이미지

🖼️ 인포그래픽

How we built saga rollbacks for Cloudflare Workflows 내용을 설명하는 본문 이미지

🖼️ 4컷 인포그래픽

How we built saga rollbacks for Cloudflare Workflows 내용을 설명하는 본문 이미지

💡 한 줄 요약

Cloudflare Workflows는 다단계 작업 실패 시 각 단계 안에 선언한 보상 로직을 역순으로 실행하는 saga rollback을 도입해, 내구성 있는 워크플로에서 부분 완료 상태를 더 명확하게 되돌릴 수 있게 했다.

📌 핵심 요약

  • Cloudflare Workflows는 재시도와 상태 지속성을 갖춘 장기 실행 다단계 애플리케이션을 만들 수 있지만, 한 단계가 실패하면 이미 완료된 외부 시스템 작업이 부분적이거나 불일치한 상태로 남을 수 있다.
  • 새로 도입된 saga rollback은 각 step.do() 호출 안에 rollback 함수를 함께 선언하게 해, 성공한 작업과 보상 작업을 별도 try-catch 블록에서 수동 추적하지 않아도 되도록 한다.
  • 롤백 함수는 일반 Workflow 단계처럼 멱등적으로 작성해야 하며, 결제 환불이나 재고 해제처럼 여러 번 호출되어도 중복 효과가 생기지 않도록 idempotency key 같은 장치를 사용해야 한다.
  • Rollback은 개별 step 오류가 발생할 때마다 즉시 시작되는 것이 아니라 Workflow가 최종적으로 실패하려는 시점에 시작되며, 등록된 핸들러는 완료 순서가 아니라 step-start의 역순으로 실행된다.
  • Cloudflare는 fluent API와 builder API를 검토했지만, step.do()의 기존 실행 시점과 Promise 의미를 흐리지 않기 위해 rollback을 step.do(..., { rollback, rollbackConfig }) 형태의 명시적 메타데이터로 추가했다.

🧩 주요 포인트

  1. Cloudflare Workflows는 재시도와 상태 지속성을 갖춘 장기 실행 다단계 애플리케이션을 만들 수 있지만, 한 단계가 실패하면 이미 완료된 외부 시스템 작업이 부분적이거나 불일치한 상태로 남을 수 있다.
  2. 새로 도입된 saga rollback은 각 step.do() 호출 안에 rollback 함수를 함께 선언하게 해, 성공한 작업과 보상 작업을 별도 try-catch 블록에서 수동 추적하지 않아도 되도록 한다.
  3. 롤백 함수는 일반 Workflow 단계처럼 멱등적으로 작성해야 하며, 결제 환불이나 재고 해제처럼 여러 번 호출되어도 중복 효과가 생기지 않도록 idempotency key 같은 장치를 사용해야 한다.
  4. Rollback은 개별 step 오류가 발생할 때마다 즉시 시작되는 것이 아니라 Workflow가 최종적으로 실패하려는 시점에 시작되며, 등록된 핸들러는 완료 순서가 아니라 step-start의 역순으로 실행된다.
  5. Cloudflare는 fluent API와 builder API를 검토했지만, step.do()의 기존 실행 시점과 Promise 의미를 흐리지 않기 위해 rollback을 step.do(..., { rollback, rollbackConfig }) 형태의 명시적 메타데이터로 추가했다.

🧠 상세 정리

1. Workflows의 기존 강점과 실패 시 문제

Cloudflare Workflows는 여러 단계로 이루어진 오래 실행되는 애플리케이션을 만들 수 있게 하며, 각 단계는 외부 시스템을 호출하고 실패 시 재시도하며 재시작 이후에도 상태를 유지할 수 있다. 이런 특성은 결제, 예약, 알림처럼 여러 시스템을 순차적으로 조율해야 하는 작업에 유용하다. 그러나 한 단계가 실패했을 때 앞선 단계들이 이미 외부 시스템에 영향을 남겼다면 문제가 생긴다. Workflows 자체는 내구성을 제공하지만, 외부 은행이나 결제 제공자에 이미 커밋된 작업까지 자동으로 원상복구할 수는 없기 때문에 부분 완료 상태를 다룰 별도 방식이 필요했다.

2. 은행 송금 예시로 설명한 saga 패턴

본문은 Bank A 계좌에서 돈을 차감하고 Bank B 계좌에 입금한 뒤 양쪽 계좌 소유자에게 이메일을 보내는 송금 Workflow를 예로 든다. 여기서 Bank A 차감이 성공한 뒤 Bank B 입금이 실패하면, 이미 Bank A 시스템에서는 돈이 빠져나간 상태가 된다. 이때 오케스트레이터가 기존 작업을 단순히 없던 일로 만들 수는 없고, 의미상 반대되는 새 작업인 Bank A 재입금을 수행해야 한다. 이렇게 원래 작업과 그 작업을 보상하는 로직을 짝으로 두는 방식이 saga 패턴이며, 이번 기능은 이를 Workflows 단계 정의 안으로 가져온 것이다.

3. 기존 수동 보상 로직의 복잡성

기능 도입 전에는 개발자가 어떤 단계가 성공했는지 변수로 직접 추적하고, catch 블록에서 완료된 작업을 역순으로 풀어내는 코드를 작성해야 했다. 예시 코드에서는 debitA와 creditB를 저장한 뒤 실패 시 reverse-credit-b, refund-debit-a 같은 보상 단계를 각각 try-catch로 감싸 실행한다. 각 undo 작업도 자체적으로 durable step이어야 하고, 실패하더라도 가능한 계속 진행해야 하며, 실패한 보상 작업에 대해서는 별도 알림까지 보내야 했다. 이런 방식은 단계가 늘어날수록 catch 블록이 커지고, 순서와 재시도, 재생 로직을 개발자가 직접 관리해야 한다는 부담을 만든다.

4. step.do 안에 함께 선언하는 rollback

새 API에서는 step.do()의 마지막 인자로 options 객체를 넘기고 그 안에 rollback 함수를 넣는다. 예를 들어 debit-account-a 단계는 bankA.debit을 실행하고, 같은 step.do 호출 안에 bankA.credit을 수행하는 rollback을 함께 둔다. credit-account-b 단계도 Bank B 입금과 그 반대 동작인 debit을 같은 위치에 선언할 수 있다. 이렇게 하면 작업을 추가할 때 해당 작업의 보상 로직도 바로 옆에 추가되므로, 별도의 거대한 catch 블록이나 수동 정렬 로직 없이 각 단계의 의미와 되돌림 방식을 함께 읽을 수 있다.

5. 멱등성과 output 처리의 중요성

본문은 rollback 함수도 일반 Workflow 단계처럼 멱등적이어야 한다고 강조한다. 결제 환불이라면 결제 제공자의 idempotency key를 사용하고, 재고 해제라면 같은 해제가 여러 번 호출되어도 중복 처리되지 않도록 만들어야 한다. 예시에서도 forward 작업과 rollback 작업 모두 transferId 기반 idempotencyKey를 사용해 재시도 시 송금이 중복되지 않게 한다. 또한 실패한 step.do도 rollback 대상이 될 수 있으므로, rollback 핸들러는 output을 받더라도 output이 undefined인 경우를 처리해야 한다. 외부 시스템에는 이미 일부 영향이 갔지만 Workflows에 반환값이 저장되기 전에 실패할 수 있기 때문이다.

6. Rollback이 시작되는 조건과 실행 순서

Rollback은 모든 step 오류마다 즉시 실행되는 것이 아니라, Workflow 자체가 최종 실패 상태로 가려는 시점에 시작된다. 사용자 코드가 오류를 잡고 계속 진행하면 Workflow는 계속 실행되며, 그 순간 rollback이 자동으로 시작되지는 않는다. 다만 어떤 step 오류를 잡고 지나간 뒤 나중에 다른 이유로 Workflow가 실패하면, 이전에 등록된 rollback 핸들러들은 여전히 실행 대상이 될 수 있다. 실행 순서는 완료 순서가 아니라 step-start의 역순이다. 병렬 단계에서는 시작 순서와 완료 순서가 달라질 수 있기 때문에, 예측 가능한 모델을 위해 시작된 단계와 완료된 단계 중 rollback 핸들러가 등록된 것들을 역방향 시작 순서로 처리한다.

7. fluent API를 선택하지 않은 이유

Cloudflare는 처음에 step.do(...).rollback(...) 같은 fluent 형태를 검토했다. 이 방식은 원래 작업과 보상 작업이 나란히 보여 읽기 좋지만, step.do()가 이미 durable step을 시작하고 step output에 대한 Promise를 반환한다는 중요한 의미와 충돌한다. Workers에서는 Promise-like 값이 promise pipelining과 관련해 특별한 의미를 가지며, 미래에 반환될 객체에 대해 미리 메서드를 호출하는 패턴도 지원된다. 이런 환경에서 .rollback() 체인은 마치 step 결과에 대해 rollback을 호출하는 것처럼 보일 수 있다. 더 큰 문제는 step.do()가 호출되는 순간 단계가 시작되는 현재 실행 모델을 흐려, rollback이 붙는지 기다리느라 실제 시작 시점이 Promise 소비 시점에 좌우될 수 있다는 점이다.

8. builder API 대신 step metadata를 택한 결론

builder 형태인 step.saga(...).do(...).rollback(...).run()도 검토됐지만, 모든 단계에 run()이 필요해지고 이를 빼먹기 쉬우며 단순한 한 단계 작업도 설정 체인처럼 보이는 단점이 있었다. 또한 새로운 step.saga() 빌더를 도입하면 기존 step.<action> 패턴에서 벗어나고, step.do()가 핵심 primitive가 아니라 오래된 API처럼 보일 위험이 있었다. 최종 선택은 step.do(..., { rollback })처럼 rollback을 step 메타데이터로 두는 방식이다. 이 형태에서는 단계가 기존처럼 시작되고, 반환 Promise는 여전히 step output을 뜻하며, 동시 실행 코드의 모델도 유지된다. rollbackConfig에는 재시도 횟수, 지연, backoff, timeout 같은 rollback 전용 설정을 둘 수 있어 lifecycle event와 로그 관점에서도 같은 durable unit의 부가 동작으로 이해할 수 있다.

🧾 핵심 주장 / 시사점

  • 이 기능의 핵심은 실패를 없애는 것이 아니라, 이미 외부 시스템에 남긴 효과를 명시적인 보상 작업으로 다루게 해 부분 실패를 관리 가능한 상태로 만드는 데 있다.
  • API 설계에서 편해 보이는 체인 문법보다 step.do()의 기존 실행 시점, Promise 의미, 동시성 추론 가능성을 보존한 점이 중요하다.
  • 실제 사용자는 rollback을 등록하는 것만으로 충분하지 않으며, 각 보상 작업을 멱등적으로 만들고 output이 없을 수 있는 실패 경로까지 고려해야 안전한 saga를 구현할 수 있다.

✅ 액션 아이템

  • 각 step.do() 호출에 rollback 함수와 rollbackConfig를 함께 등록해 보상 동작을 단계별 선언 규칙으로 고정한다.
  • 워크플로 실패 시점에 롤백이 시작됨을 전제로, step-start 역순 실행 규칙에 맞춰 부분 완료 상태의 복구 범위를 재정의한다.
  • 롤백 함수는 멱등성으로 설계하고 결제 환불·재고 해제에 idempotency key를 붙여 반복 호출 시 중복 효과를 차단한다.

❓ 열린 질문

  • 개별 step 오류 발생 즉시가 아닌 최종 실패 시점 롤백 구조에서 실패 감시는 어디에서 시작하고 끝내야 하는가?
  • step-start 역순 롤백이 완료 의존이 강한 단계 간 격리에 미치는 영향은 어떤 기준으로 판단할 것인가?
  • idempotency key가 충분한지, 환불·재고해제 API에서 반복 호출을 안전하다고 볼 수 있는 계량 기준은 무엇인가?

관련 문서

공통 태그와 주제 흐름을 기준으로 같이 보면 좋은 문서를 이어서 제안합니다.