내구성이 있는 실행(Durable Execution)은 설치하는 것입니다
요약
분산 시스템 환경에서 프로세스 중단 시 작업의 연속성을 보장하는 '내구성 있는 실행(Durable Execution)'의 중요성을 다룹니다. 기존의 이벤트 기반 아키텍처가 가진 높은 운영 비용과 복잡성을 지적하며, 비즈니스 로직과 인프라적 배관 작업을 분리해야 함을 강조합니다.
핵심 포인트
- 컨테이너 종료나 배포 시에도 작업 상태를 유지하는 내구성이 필수적임
- 기존 이벤트 기반 방식은 멱등성, 사가, 상태 머신 등 높은 운영 비용을 초래함
- 비즈니스 로직과 인프라적 배관(plumbing) 작업을 분리하여 설계해야 함
- 핀테크와 같이 데이터 일관성이 중요한 도메인에서 특히 중요함
하나의 프로세스가 다단계 작업을 시작합니다. 1단계는 고객을 검증합니다. 2단계는 내부 기록을 생성합니다. 3단계는 돈을 이동시키거나, 구독을 변경하거나, 파트너에게 되돌릴 수 없는 요청을 보내거나, 귀하의 비즈니스가 중요하다고 간주하는 무엇이든 수행합니다. 그러고 나서 프로세스가 종료됩니다.
정중하게 종료되는 것이 아닙니다.
컨테이너가 살해됩니다. 배포(deploy)가 롤링됩니다. 호스트가 사라집니다. 워커(worker)가 작업 도중에 재시작됩니다.
이제 질문은 단순하면서도 끔찍합니다:
다시 실행될 때 어떤 일이 벌어질까요?
만약 3단계가 이미 수행되었다면, 1단계부터 재시작하는 것은 비용이 많이 드는 작업을 중복할 수 있습니다. 너무 많이 건너뛰면 고객을 반쯤 활성화된 상태로 남겨둘 수 있습니다. 맹목적으로 재시도하면 외부 제공업체가 동일한 요청을 두 번 수락할 수도 있습니다.
이것이 많은 핀테크(fintech) 작업의 형태입니다. 계좌 개설, 구독 활성화, 신용 대출, 상환 결제, KYC 갱신, 예약된 자금 이체(sweeps), 원장 기입(ledger posting) 등이 있습니다. 이것들은 하나의 데이터베이스 트랜잭션(database transaction)이 아닙니다. 이것들은 함수 호출(function calls)인 척하는 작은 비즈니스 이야기들입니다.
과거의 해답은 규율(discipline)이었습니다
아메리카 대륙의 한 핀테크 기업에서, 공개된 아키텍처(architecture) 이야기는 이벤트 기반(event-driven)이었습니다: Kafka, 서비스, 비동기 처리(async processing), 이벤트를 통한 일관성(consistency), 그리고 멱등성(idempotency)에 대한 신중한 고민이 그것입니다. 그러한 스타일은 은행 규모의 환경에서는 말이 됩니다.
하지만 작업의 지루한 부분은 결코 단순히 "이벤트를 발행하는 것"이 아니었습니다. 진짜 작업은 각 단계가 무엇을 의미하는지, 어떤 메시지가 진실의 원천(source of truth)인지, 중복을 어떻게 식별할지, 컨슈머(consumer)가 안전하게 재실행(replay)되도록 어떻게 만들지, 그리고 이미 경계를 넘어선 것에 대해 어떻게 보상(compensate)할지를 결정하는 것이었습니다.
결국 여러분은 사가(sagas), 상태 머신(state machines), 멱등성 키(idempotency keys), 아웃박스 테이블(outbox tables), 재시도 토픽(retry topics), 조정 작업(reconciliation jobs), 감사 추적(audit trails), 대시보드, 알림, 그리고 런북(runbooks)을 갖게 됩니다.
이것은 비판이 아닙니다. 진지한 금융 시스템에서 이러한 규율은 업무의 일부입니다. "큐(queue)는 결과적 일관성(eventually consistent)을 가집니다"라고 말하며 손을 휘저어 넘기고 점심을 먹으러 갈 수는 없습니다.
하지만 이러한 메커니즘을 소유하는 데에는 운영 비용이 따릅니다. 모든 팀은 동일한 실패 모드(failure modes)를 기억해야 합니다. 모든 새로운 워크플로(workflow)는 동일한 스캐폴딩(scaffolding)을 다시 구축해야 합니다. 모든 장애 상황(incident)마다 다음과 같은 질문을 던지게 됩니다: 해당 단계가 완료되었는가? 이벤트가 발행되었는가? 재시도(retry) 과정에서 중복이 발생했는가? 그리고 현재 고객의 상태가 실제와 일치하는가?
고통스러운 점은 이러한 기계적 장치들의 대부분이 특정 제품에 국한된 것이 아니라는 사실입니다.
보상 로직(compensation logic)은 제품 특유의 것입니다. 단계의 경계(step boundaries)도 제품 특유의 것입니다. 송금은 재시도할 수 있지만 파트너 호출에는 멱등성 키(idempotency key)가 필요하다는 결정 또한 제품 특유의 것입니다. 하지만 "워커(worker)가 충돌한 후 완료된 3단계를 기억하라"는 것은 배관(plumbing) 작업입니다.
그리고 배관 작업은 결국 하나의 제품이 됩니다.
dbos와 temporal이 기본값을 바꾸다
이것이 바로 DBOS나 Temporal 같은 프레임워크가, 설령 당장 도입하지 않더라도 주목할 가치가 있는 이유입니다.
Temporal의 모델은 워크플로(workflows)와 액티비티(activities)를 중심으로 구축되었습니다. Temporal 서비스는 각 워크플로 실행에 대해 내구성이 있는 이벤트 히스토리(durable event history)를 저장합니다. 워커(worker)가 충돌하거나 재시작될 때, 워크플로 코드는 히스토리로부터 재생(replay)될 수 있으며, 완료된 액티비티는 단순히 반복되지 않습니다. 서비스가 무엇이 일어났는지에 대한 기록을 가지고 있기 때문입니다.
DBOS는 다른 제품 형태를 취합니다. DBOS Transact는 애플리케이션 내부에서 실행되는 오픈 소스 라이브러리로 포지셔닝되어 있습니다. 현재 DBOS 사이트에는 Transact가 기존의 Postgres 데이터베이스를 사용하여 워크플로 상태와 실행 히스토리를 저장하고 복구한다고 명시되어 있습니다. 이들의 유료 모델은 DBOS Conductor, 툴링(tooling), 지원 및 호스팅 옵션을 중심으로 하며, Pro 및 Teams 플랜은 체크포인트(checkpoint) 사용량을 기준으로 가격이 책정됩니다.
이러한 차이는 중요합니다.
Temporal은 워크플로 서비스를 직접 실행하거나 비용을 지불할 것을 요구합니다. Temporal Cloud의 가격 책정은 사용량 기반(consumption-based)이며, 주로 액션(actions)과 스토리지(storage)를 중심으로 합니다. 현재 최소 Cloud 플랜은 월 $100로 기재되어 있습니다.
DBOS는 보다 Postgres 네이티브(Postgres-native)적인 승부수를 던지고 있습니다. 오픈 소스 라이브러리는 애플리케이션 코드 내에서 내구성이 있는 워크플로를 제공하며, Conductor는 모니터링, 복구, 버전 관리, 알림 및 지원과 같은 운영 툴링(operational tooling)을 추가합니다.
흥미로운 점은 두 가지 모두 과거에는 내부 기능이었던 동일한 역량을 패키지화했다는 것입니다:
이 다단계 작업(multi-step job)은 프로세스 종료(process death) 상황에서도 생존합니다.
이 문장은 과거에는 맞춤형 플랫폼 작업(bespoke platform work)을 의미했습니다. 하지만 이제는 프레임워크로 시작할 수 있습니다.
어려운 부분이 사라진 것은 아니다
여기에 함정이 있으며, 이는 인프라가 개선될 때마다 매번 나타나는 바로 그 함정입니다.
사람들은 "프레임워크가 무슨 일이 일어났는지 기억한다"는 것과 "시스템이 이제 무엇이 일어나야 하는지 알고 있다"는 것을 혼동합니다.
그렇지 않습니다.
만약 워크플로(workflow)가 고객에게 비용을 청구하고, 구독을 활성화하며, 액세스 권한을 프로비저닝하고, 이메일을 보내고, 분석 데이터를 업데이트한다면, 프레임워크는 해당 워크플로를 재개 가능(resumable)하게 만드는 데 도움을 줄 수 있습니다. 프레임워크는 단계 완료를 기록하고, 액티비티(activities)를 재시도하며, 실행이 어디서 중단되었는지 보여주고, 히스토리를 로그(logs) 속에 파묻는 대신 가시화할 수 있습니다.
하지만 고객 청구가 활성화 전에 일어나야 하는지, 아니면 활성화 후에 일어나야 하는지는 알려줄 수 없습니다.
실패한 활성화가 환불, 재시도, 수동 검토, 또는 유예 기간(grace period)을 트리거해야 하는지도 알려줄 수 없습니다. 파트너 API가 멱등성 키(idempotency key)를 수용한다고 해서 실제로 멱등성(idempotent)을 갖는지도 알려줄 수 없습니다. 당신의 "단계(step)"가 하나의 비즈니스 액션인지, 아니면 트렌치코트를 입은 세 개의 비즈니스 액션인지도 알려줄 수 없습니다.
그것은 여전히 설계 작업(design work)입니다.
제가 현재 협업하고 있는 걸프 지역의 한 핀테크(fintech) 기업에서는, 이것이 일반적인 제품 워크플로에서 발생합니다. 예약된 스윕(sweep) 작업이 흥미로운 이유는 단순히 타이머가 작동했기 때문이 아닙니다. 시스템이 어떤 계좌가 고려되었는지, 어떤 계좌가 건너뛰어졌는지, 어떤 외부 호출이 성공했는지, 그리고 배포(deploy) 후 프로세스가 깨어났을 때 무엇이 일어나야 하는지를 알아야 하기 때문에 흥미로운 것입니다.
내구성이 있는 실행(Durable execution)은 그 부분에서 큰 도움을 줍니다. Postgres에 기록된 완료된 단계는 로그 한 줄과 기도(a prayer)에 의존하는 것보다 훨씬 낫습니다. 워크플로 콘솔(workflow console)은 단서를 찾기 위해 워커(worker)에 SSH로 접속하는 것보다 낫습니다. 하지만 유용한 대화의 주제는 상위 계층으로 이동합니다.
"재시도 테이블을 어떻게 만들 것인가?"라고 묻는 대신, 팀은 "재시도가 보상 (compensation)으로 바뀌는 경계는 어디인가?"라고 묻습니다. "충돌 (crash) 후에 어떻게 재개할 것인가?"라고 묻는 대신, 팀은 "이 고객에게 재개 (resumed)된다는 것은 무엇을 의미하는가?"라고 묻습니다.
그것이 엔지니어링 시간을 더 잘 사용하는 방법입니다.
멱등성 (idempotency)은 여전히 경계에 존재합니다
어떤 내구성이 있는 실행 (durable execution) 제안에서도 제가 주의 깊게 보는 한 가지는 멱등성 (idempotency)입니다.
프레임워크 경계 내부에서는 매우 강력한 보장을 받을 수 있습니다. 완료된 워크플로 (workflow) 단계가 기록될 수 있습니다. 액티비티 (activity) 결과가 기억될 수 있습니다. 재시도 (retries)를 제어할 수 있습니다. 충돌 (crash) 후 실행을 재개할 수 있습니다.
외부 경계에서는 현실이 더 복잡합니다.
결제 제공업체, 은행 파트너, 이메일 시스템, ID 공급업체, 카드 프로세서 등은 모두 중복 요청에 대해 각기 다른 생각을 가지고 있습니다. 어떤 곳은 멱등성 (idempotency)을 잘 지원합니다. 어떤 곳은 문서상으로만 지원합니다. 어떤 곳은 그들의 부수 효과 (side effect)와 귀하의 응답 사이에 타임아웃 (timeout)이 발생하기 전까지만 지원합니다.
경계 호출 (boundary calls)에는 여전히 안정적인 요청 식별자 (request identifiers), 제공업체 참조 (provider references), 그리고 대조 (reconciliation)가 필요합니다. 타임아웃이 "아무 일도 일어나지 않음"을 의미하는지, "무언가 일어났지만 귀하가 확인하지 못함"을 의미하는지, 아니면 "나중에 확인"을 의미하는지 여전히 알아야 합니다.
내구성이 있는 실행 (durable execution)은 그러한 로직이 새어 나가는 지점의 수를 줄여줍니다.
하지만 분산 시스템 (distributed systems)을 폐지하는 것은 아닙니다.
더 넓은 패턴
제가 이 카테고리를 좋아하는 이유는 이것이 소프트웨어에서 계속 반복되는 패턴에 부합하기 때문입니다. 처음에는 신뢰성 속성 (reliability property)이 수공업의 영역입니다. 강력한 팀은 선택의 여지가 없기 때문에 이를 내부적으로 구축합니다. 그러다 충분히 많은 팀이 동일한 고통을 겪게 되면, 그 속성은 제품 (product)이 됩니다.
관측성 (Observability)이 그랬습니다. 피처 플래그 (Feature flags)가 그랬습니다. 비밀 관리 (Secrets management)가 그랬습니다. CI/CD가 그랬습니다. 정책 코드화 (Policy-as-code)가 그랬습니다. 이제 내구성이 있는 실행 (durable execution)이 그 일을 하고 있습니다.
지루한 제어 평면 (control-plane) 속성이 구매하거나, 설치하거나, 외주를 줄 수 있는 무언가가 됩니다.
그것은 좋은 일입니다. 모든 팀이 실수로 자신만의 미니 워크플로 엔진 (workflow engine)을 구축하는 것을 보고 싶지 않습니다.
하지만 업무가 사라지는 것은 아닙니다. 이동할 뿐입니다.
가치는 더 이상 당신의 워커 (worker)가 재시작 후에도 생존할 수 있음을 증명하는 데 있지 않습니다. 가치는 어떤 워크플로 (workflow)가 내구성 (durability)을 가져야 하는지, 비즈니스 관점에서 어떤 단계가 원자적 (atomic)인지, 어떤 실패는 재시도 (retry)해야 하고 어떤 실패는 보상 (compensate)해야 하는지, 그리고 고객의 결과물에 대한 책임이 누구에게 있는지를 결정하는 데 있습니다.
바로 이 지점에서 내구성이 있는 실행 (durable execution)이 흥미로워집니다.
DBOS나 Temporal이 실패를 사라지게 만들기 때문이 아닙니다.
그들이 실패를 충분히 가시화하여, 우리가 가장 어려운 부분이 재시도 루프 (retry loop)였다고 가장하는 것을 멈출 수 있게 만들기 때문입니다.
references
- DBOS Transact
- DBOS Pricing
- Temporal Event History documentation
- Temporal Cloud pricing documentation
제 프로젝트를 테스트하기 위해 저는 Railway를 사용합니다. 시작할 때 20달러(USD)를 받고 싶다면, 이 링크를 사용하세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기