idempotency

2 개의 포스트

cloudflare3분 읽기큐레이션 요약

Cloudflare Workflows를 위한 사가 롤백 구축 방법

Cloudflare Workflows에 Saga 패턴 기반의 롤백 기능이 추가되어, 각 `step.do()`에 보상 작업을 함께 선언할 수 있게 되었습니다. 여러 외부 시스템을 거치는 워크플로에서 중간 단계가 실패해도 이전 작업을 역순으로 되돌릴 수 있으며, 롤백 자체도 내구성 있는 단계로 실행됩니다. 이를 통해 개발자가 별도의 `try-catch`, 실행 이력 추적, 수동 롤백 순서 관리를 구현할 필요가 줄어듭니다. ## 분산 작업에서 롤백이 필요한 이유 - Workflow는 여러 단계에 걸쳐 외부 시스템을 호출하고, 각 단계의 상태를 저장하며 실패 시 재시도합니다. - 그러나 이미 완료된 외부 작업은 단순히 “취소”할 수 없습니다. - 예: Bank A에서 출금이 성공한 뒤 Bank B 입금이 실패하면, Bank A의 출금을 삭제하는 대신 다시 입금해야 합니다. - 원래 작업과 이를 의미적으로 되돌리는 보상 작업의 조합을 Saga 패턴이라고 합니다. - 기존에는 개발자가 성공한 단계를 추적하고, 실패 시 어떤 작업을 어떤 순서로 취소할지 직접 관리해야 했습니다. ## `step.do()`에 보상 로직 선언 - 이제 `step.do()`의 마지막 인자로 `rollback` 함수를 전달할 수 있습니다. ```ts await step.do( "debit-bank-a", () => bankA.debit(from, amount), { rollback: async ({ output }) => bankA.credit(from, amount, output.id), } ); ``` - 각 정방향 작업과 롤백 작업이 같은 위치에 정의됩니다. - 새로운 단계를 추가할 때 해당 단계의 보상 로직도 함께 추가할 수 있습니다. - 별도의 대형 `catch` 블록이나 성공 단계 추적 변수, 수동 실행 순서 관리가 필요하지 않습니다. - 롤백 함수는 정방향 작업의 결과인 `output`을 받아 보상 작업에 활용할 수 있습니다. ## 롤백 실행 순서와 실패한 단계 처리 - 어떤 단계에서 오류가 발생하면, 롤백 핸들러는 단계가 시작된 순서의 역순으로 실행됩니다. - 출금 → 입금 → 알림 순서라면, 롤백은 입금 취소 → 출금 환불 순서입니다. - 오류가 발생한 단계 자체도 롤백 대상이 될 수 있습니다. - 외부 시스템에는 작업이 반영됐지만, 결과를 Workflow에 반환하기 전에 단계가 실패할 수 있기 때문입니다. - 예를 들어 결제 제공자가 금액을 승인한 뒤 `chargeId`를 반환하기 전에 오류가 발생할 수 있습니다. - 따라서 롤백 함수는 `output === undefined`인 경우도 안전하게 처리해야 합니다. - 사용자가 오류를 잡고 Workflow를 정상적으로 계속 진행하면 롤백은 시작되지 않습니다. - 다만 오류를 잡은 뒤 Workflow가 나중에 다른 이유로 실패하면, 그때까지 등록된 롤백 핸들러가 역순으로 실행될 수 있습니다. ## 롤백도 내구성 있는 작업으로 실행 - 롤백은 단순한 메모리상의 정리 코드가 아니라 Workflow의 내구성 모델에 따라 실행됩니다. - 재시작이나 일시적인 장애가 발생해도 롤백 작업을 추적하고 재시도할 수 있습니다. - 롤백 과정에서 하나의 보상 작업이 실패하더라도 이후 롤백을 계속 진행할 수 있도록 설계해야 합니다. - 롤백 실패는 운영자가 대응할 수 있도록 알림이나 별도 모니터링을 연결하는 것이 필요합니다. ## 멱등성 보장의 중요성 - 일반 Workflow 단계와 마찬가지로 롤백 함수도 멱등적이어야 합니다. - 같은 롤백이 여러 번 실행되어도 결과가 중복 적용되면 안 됩니다. - 권장 방식: - 결제 환불에는 결제 제공자의 멱등성 키 사용 - 재고 해제는 여러 번 호출해도 한 번만 해제되도록 구현 - 출금·입금과 롤백 각각에 고유한 멱등성 키 부여 - 예시에서는 다음과 같이 작업별 키를 사용합니다. ```ts `${transferId}:debit-account-a` `${transferId}:rollback-debit-account-a` ``` - 이를 통해 Workflow 재시도나 롤백 재실행으로 인해 동일한 이체가 중복 처리되는 것을 방지합니다. ## 실용적인 적용 권장사항 - 외부 시스템을 변경하는 모든 단계에 가능한 한 명시적인 롤백 함수를 함께 정의하세요. - 롤백 함수는 `output`이 없거나 일부 작업만 반영된 상황도 처리해야 합니다. - 정방향 작업과 롤백 모두에 안정적인 멱등성 키를 사용하세요. - 롤백 실패는 조용히 무시하지 말고 알림, 재처리 큐, 운영 대시보드 등으로 추적하세요. - Saga 롤백은 트랜잭션을 원자적으로 만드는 기능이 아니라, 실패 후 상태를 보정하는 보상 처리机制이므로 외부 API의 보상 연산을 신중히 설계해야 합니다.

원문 읽기(새 탭에서 열림)
toss원문

레거시 결제 원장을 확장 가능한 시스템으로 (새 탭에서 열림)

토스페이먼츠는 20년 된 레거시 결제 원장의 구조적 한계와 도메인 간 강한 결합을 해결하기 위해 MySQL 기반의 신규 원장 시스템을 구축했습니다. 데이터 불변성을 보장하는 INSERT-only 원칙과 이벤트 기반 아키텍처를 도입하여 복합 결제 지원 등 비즈니스 확장성을 확보했습니다. 이 과정에서 발생한 데이터 불일치와 타임아웃 문제를 해결하며 시스템의 자가 회복 능력을 강화하고 안정적인 운영 환경을 마련했습니다. ### 레거시 원장 시스템의 한계와 과제 - **데이터 구조의 불일치:** 결제수단별로 테이블 구조가 다르고, 동일한 성격의 데이터가 서로 다른 테이블에 저장되어 유지보수와 온보딩에 큰 비용이 발생했습니다. - **도메인 간 강한 결합:** 결제, 정산, 회계 등 여러 서비스가 하나의 원장 테이블과 컬럼을 공유하여, 작은 기능 수정 시에도 전사적인 영향도 분석이 필요했습니다. - **구조적 확장성 부족:** 결제와 결제수단이 1:1 관계로 묶여 있어, 더치페이나 복합 결제(카드+포인트)와 같은 현대적인 결제 시나리오를 지원할 수 없었습니다. ### 신규 원장 설계의 3가지 전략 - **데이터 불변성과 일관성:** 모든 승인 내역을 공통 테이블(`approve`)에 저장하고, 수정 대신 INSERT-only 방식을 채택하여 데이터의 정합성을 높이고 데드락을 방지했습니다. - **이벤트 기반의 도메인 분리:** 각 도메인이 직접 DB를 조회하는 대신 Kafka 이벤트를 구독하여 데이터를 처리하게 함으로써 도메인 간 의존성을 제거했습니다. - **결제와 승인 개념의 분리:** '결제'는 주문의 상태를, '승인'은 실제 결제수단의 실행을 의미하도록 분리하여 하나의 결제에 여러 승인 수단이 연결될 수 있는 유연한 구조를 만들었습니다. ### 무중단 마이그레이션 및 정합성 검증 - **비동기 점진적 적재:** 실서비스 장애를 방지하기 위해 기존 원장에 먼저 저장한 후, 신규 원장에는 별도의 ThreadPool을 통한 비동기 방식으로 데이터를 적재했습니다. - **검증 배치 운영:** 비동기 적재 중 발생할 수 있는 누락을 방지하기 위해, 매 5분마다 Read-Only DB를 기반으로 기존 원장과 신규 원장의 데이터를 비교하고 보정하는 배치를 실행했습니다. - **고성능 이관 작업:** 수억 건의 데이터 이관을 위해 Bulk Insert를 도입하고, 네트워크 지연 최소화를 위해 마이그레이션 서버를 DB와 동일한 가용 영역(AZ)에 배치했습니다. ### 운영 중 장애 대응과 시스템 고도화 - **쿼리 최적화:** 옵티마이저의 판단 오류로 발생한 풀 스캔(Full Scan) 문제를 인덱스 힌트(Index Hint) 추가와 롤백 시스템을 통해 빠르게 해결했습니다. - **타임아웃 및 정합성 관리:** MSA 구조에서 서버 간 타임아웃 설정을 일치시키고, 외부 원천사와의 상태 불일치를 해결하기 위한 망취소(Network Cancellation) 로직을 강화했습니다. - **이벤트 처리의 신뢰성:** 아웃박스(Outbox) 패턴과 로그 기반 복구를 통해 이벤트 누락을 방지하고, 헤더에 멱등키를 포함해 중복 이벤트 처리 문제를 해결했습니다. 신규 시스템으로의 전환은 단순한 DB 교체가 아니라 시스템의 지속 가능성을 확보하는 과정입니다. 초기 설계의 완벽함보다 중요한 것은 운영 중 발생하는 예외 상황에 시스템이 스스로 대응하고 회복할 수 있는 '자가 회복 구조'를 갖추는 것이며, 이를 위해 데이터 보정 배치와 로깅 시스템 같은 안전장치를 반드시 고려해야 합니다.