Cadence 완벽 가이드 — Uber 워크플로우 엔진
이 글의 핵심
Cadence는 Uber가 공개한 내구성 있는 워크플로우 실행 엔진으로, 마이크로서비스·레거시 시스템을 아우르는 업무 절차를 코드로 표현합니다. Domain·Tasklist, Workflow·Activity, 시그널·쿼리, 재시도 정책, Temporal과의 관계, 주문 처리 실전 예제를 다룹니다.
이 글의 핵심
Cadence는 Uber가 오픈소스로 공개한 분산 워크플로우 오케스트레이션 플랫폼입니다. 장애·배포·프로세스 재시작 이후에도 동일한 비즈니스 절차를 재현하려면, 실행 상태를 애플리케이션 프로세스 밖에 지속적으로 기록하고, 워커가 그 기록을 따라 결정론적으로 워크플로우 코드를 다시 실행하는 모델이 필요합니다. Cadence는 그 역할을 서비스형 실행 엔진과 이벤트 히스토리로 제공합니다.
본문에서는 Domain·Tasklist로의 배치, Workflow·Activity의 책임 분리, 외부와의 비동기 연동을 위한 시그널·쿼리, 외부 시스템 특성에 맞춘 재시도 정책, 커뮤니티에서 널리 쓰이는 Temporal과의 비교, 그리고 주문 처리를 중심으로 한 실전 워크플로우를 정리합니다. Cadence는 Go·Java SDK가 성숙한 편이므로, 예제는 Go 스타일 위주로 제시하되 개념은 언어에 무관하게 적용할 수 있습니다.
1. Cadence가 해결하는 문제
분산 환경에서 “주문 생성 → 결제 → 재고 확보 → 배송 요청” 같은 절차를 직접 구현하면, 단계마다 상태 저장, 재시도, 타임아웃, 멱등성, 수동 복구 스크립트가 코드베이스 곳곳에 흩어집니다. Cadence는 이런 횡단 관심사를 워크플로우 실행 서비스와 기록된 이벤트 히스토리로 모읍니다. 워커 프로세스가 종료되어도 서버가 히스토리를 보존하고, 새 워커가 같은 워크플로우 정의로 리플레이(replay)하여 진행 지점을 복원합니다.
운영 관점에서는 배치 스크립트와 달리, 실행 중인 절차를 식별자(워크플로우 ID) 단위로 조회·시그널·취소할 수 있다는 점이 큽니다. 지원·정산·현장 운영이 “어떤 주문이 어느 단계에서 멈췄는지”를 제품처럼 다룰 수 있게 됩니다.
2. 아키텍처 개요
Cadence 배포는 일반적으로 다음 구성요소로 이해할 수 있습니다.
- Frontend Service: 클라이언트·워커가 붙는 gRPC 진입점. 워크플로우 시작, 시그널, 쿼리 요청을 받습니다.
- History Service: 워크플로우 실행별 이벤트 히스토리를 저장·재생하는 핵심입니다. 내구성의 근거입니다.
- Matching Service: Tasklist에 대한 태스크 큐잉과 워커로의 전달을 담당합니다.
- Worker: 사용자가 운영하는 프로세스로, 워크플로우 태스크와 액티비티 태스크를 폴링해 실행합니다. 수평 확장의 단위입니다.
데이터 플로우는 “클라이언트가 실행 시작 → 히스토리에 이벤트 적재 → 매칭 서비스가 Tasklist에 태스크 적재 → 워커가 처리 → 결과가 다시 히스토리에 기록”의 순환입니다. 이 고리가 끊기지 않는 한 한 번 시작한 절차는 정의된 정책 안에서 완료까지 추적됩니다.
3. 핵심 개념
3.1 내구성 있는 실행과 결정론
Cadence의 워크플로우 함수는 동일한 이벤트 히스토리를 입력으로 리플레이할 때 항상 같은 분기, 같은 Activity 스케줄 순서를 재현해야 합니다. 이를 워크플로우 결정론(determinism)이라고 부릅니다. 따라서 워크플로우 본문에는 다음을 두지 않는 것이 안전합니다.
- 네트워크·DB·메시지 큐에 대한 직접 호출
- 비고정 시드 난수, “지금 시각”을 임의로 읽는 코드(언어별로 안전한 API가 제공됨)
- 워크플로우 외부와 공유되는 가변 전역 상태에 의존하는 분기
부작용은 Activity로 위임하고, 워크플로우는 스케줄링·분기·타이머·시그널 대기 등 오케스트레이션만 담당합니다.
3.2 Workflow 실행과 Run ID
Workflow는 등록된 함수 형태의 절차 정의입니다. 클라이언트가 Workflow ID(비즈니스 키에 매핑되는 경우가 많음)로 실행을 시작하면, 서버는 Run ID로 각 실행 시도를 구분합니다. 재시작·Continue-As-New 등에 따라 한 Workflow ID에 여러 Run이 이어질 수 있습니다. 운영 로그·지원 티켓에는 Workflow ID와 Run ID를 함께 남기는 편이 추적에 유리합니다.
3.3 Activity
Activity는 HTTP 호출, DB 트랜잭션, 메시지 발행 등 부작용이 있는 작업 단위입니다. Activity는 자체 타임아웃, 하트비트, 재시도 정책을 가질 수 있어 외부 API SLA와 맞출 수 있습니다. 동일 Activity가 여러 번 실행될 수 있으므로 멱등 키 설계가 필수에 가깝습니다.
3.4 Child Workflow와 Continue-As-New
실행 기간이 길거나 이벤트가 많아지면 히스토리가 커져 리플레이 비용이 증가합니다. Continue-As-New는 현재 실행을 마감하고 동일 Workflow ID로 새 Run을 이어 가며 히스토리를 줄이는 패턴입니다. 하위 절차를 독립 생명주기로 묶으려면 Child Workflow를 사용합니다.
4. Domain과 Tasklist
4.1 Domain
Domain은 Cadence에서 워크플로우 실행을 묶는 최상위 격리 경계입니다. 팀·제품·환경(예: 프로덕션·스테이징)별로 Domain을 나누면 설정(히스토리 보존, 아카이브 정책 등)과 운영 권한을 분리하기 쉽습니다. Workflow ID는 Domain 안에서 유일하면 되며, 다른 Domain과는 충돌하지 않습니다.
도메인 설계 시 고려할 점은 다음과 같습니다.
- 규제·데이터 주권: 특정 지역 사용자 데이터만 분리해야 하면 Domain 단위 분리가 후보입니다.
- 장애 폭발 반경: 한 Domain의 과부하가 다른 제품 라인까지 흔들리지 않도록 경계를 나눕니다.
- 배포·버전: 워커 바이너리와 Domain 설정을 함께 문서화해 “어느 Domain이 어떤 워크플로우 타입을 처리하는지”를 명확히 합니다.
4.2 Tasklist
Tasklist는 워커가 태스크를 가져갈(poll) 큐의 논리적 이름입니다. 워크플로우 태스크와 액티비티 태스크는 각각 Tasklist에 스케줄되며, 워커는 하나 이상의 Tasklist를 구독합니다.
실무에서는 보통 다음처럼 나눕니다.
- 서비스·팀 단위:
orders-worker,payments-worker처럼 바이너리 경계와 맞춥니다. - 우선순위·SLA: 실시간 주문과 배치 정산을 다른 Tasklist로 분리해 상호 적체를 줄입니다.
- 배포 격리: 카나리 배포 시 일부 워커만 새 Tasklist를 구독하게 하여 점진 전환을 할 수 있습니다.
Tasklist 이름은 “문자열 하나”에 불과하지만, 운영에서는 모니터링·알람·오토스케일 정책과 직결되므로 명명 규칙을 팀 표준으로 두는 것이 좋습니다.
5. Workflow와 Activity — 계약과 실행
5.1 워크플로우 등록
워커 프로세스는 시작 시 워크플로우 함수와 액티비티 함수를 이름과 함께 등록합니다. 클라이언트가 해당 이름으로 실행을 시작하면, 히스토리에 스케줄 이벤트가 쌓이고 Tasklist로 워크플로우 태스크가 전달됩니다.
5.2 Activity 옵션
Activity 실행에는 보통 다음이 포함됩니다.
- ScheduleToStartTimeout: 태스크가 큐에 올라온 뒤 워커에 할당되기까지의 상한. 워커 부족·적체를 드러냅니다.
- StartToCloseTimeout: 워커가 Activity를 실행해 완료하기까지의 상한. 비즈니스 로직의 상한입니다.
- HeartbeatTimeout: 장시간 작업이 살아 있는지 검사합니다. 하트비트와 함께 쓰입니다.
주문 도메인에서는 “결제 게이트웨이 응답 대기”와 “창고 WMS 연동”의 타임아웃이 다르므로, Activity 타입별로 옵션을 분리하는 것이 일반적입니다.
5.3 버전과 호환성
워크플로우 소스 변경은 리플레이와 충돌할 수 있습니다. 장기 실행 인스턴스가 남아 있는 동안에는 조건부 분기(예: 빌드 ID·도메인 상수·명시적 버전 플래그)로 구·신 로직을 공존시키고, 구 실행이 소멸한 뒤 분기를 정리합니다. Activity 입출력 스키마 변경 시에는 하위 호환 필드 추가 또는 새 Activity 이름으로 전환하는 방식이 안전합니다.
6. 시그널과 쿼리
6.1 시그널(Signal)
시그널은 실행 중인 워크플로우 인스턴스에 비동기로 전달되는 외부 이벤트입니다. 예를 들어 “결제 웹훅 수신”, “고객 취소 요청”, “재고 확보 완료 알림” 등을 시그널로 모델링합니다. 워크플로우는 시그널 채널을 통해 이벤트를 받고, Selector 패턴(Go) 등으로 여러 소스(타이머·시그널)를 함께 기다립니다.
시그널 핸들러도 리플레이 대상이므로, 결정론적으로 워크플로우 내부 상태만 갱신해야 합니다. 시그널 순서가 비즈니스 규칙과 맞는지(예: 취소가 결제 확정보다 먼저 처리되어야 하는지)를 명시적으로 검증하는 편이 안전합니다.
6.2 쿼리(Query)
쿼리는 워크플로우의 현재 상태를 읽기 전용으로 조회하는 경로입니다. 지원 도구·내부 대시보드에서 “이 주문이 어느 단계인지”를 보여 줄 때 사용합니다. 쿼리 핸들러는 부작용을 일으키지 않아야 하며, 워크플로우가 유지하는 상태의 일관된 스냅샷을 반환하도록 설계합니다.
7. 재시도 정책
7.1 Activity 재시도
외부 API는 타임아웃·일시적 오류·레이트 리밋으로 실패합니다. Cadence에서는 Activity에 RetryPolicy를 붙여 다음을 조정합니다.
- InitialInterval: 첫 재시도 전 대기
- BackoffCoefficient: 지수 백오프 배수
- MaximumInterval: 백오프 상한
- MaximumAttempts: 최대 시도 횟수(구현·버전에 따라 0이 무제한 의미일 수 있어 문서 확인)
- NonRetriableErrorReasons: 재시도해도 의미 없는 오류(예: 잘못된 주문 ID)는 즉시 실패로 분류
재시도와 별개로, 동일 Activity 실행이 중복될 수 있다는 전제에서 멱등성을 설계합니다. 결제·재고에서는 외부 시스템이 지원하는 요청 키·토큰을 반드시 활용합니다.
7.2 Workflow Task와 워커 측 실패
워크플로우 결정(decision) 한 사이클이 너무 오래 걸리거나 버그로 인해 리플레이가 실패하면 Workflow Task 수준의 오류로 드러납니다. 이는 Activity 실패와 성격이 다르므로, 워크플로우 본문은 가벼운 제어 로직만 두고 무거운 처리는 Activity로 넘기는 것이 좋습니다.
8. Temporal과의 비교
Temporal은 Cadence 코드베이스를 포크하여 발전시킨 별도의 프로젝트입니다. 핵심 아이디어—이벤트 히스토리, 결정론적 워크플로우, Activity, 시그널·쿼리, Task Queue—는 같지만, 제품 로드맵·SDK·운영형 서비스는 갈라졌습니다.
| 구분 | Cadence | Temporal |
|---|---|---|
| 기원 | Uber 오픈소스 | Cadence 포크 이후 독립 커뮤니티 |
| 네임스페이스 | Domain | Namespace(개념 유사) |
| 태스크 라우팅 | Tasklist | Task Queue |
| SDK 성숙도 | Go·Java 중심 | Go·Java·TypeScript·Python·PHP·.NET 등 광범위 |
| 매니지드 서비스 | 자체 운영·커뮤니티 배포 | Temporal Cloud 등 |
언제 Cadence를 볼까: 레거시 시스템이 Cadence 위에 이미 올라가 있거나, Uber 오픈소스 문서·패턴과의 정합이 중요한 경우입니다. 신규 표준을 고른다면 팀이 사용하는 언어 SDK·클라우드 운영 요구·엔터프라이즈 지원을 기준으로 Temporal과 Cadence를 비교하는 것이 일반적입니다. 개념 이해를 위해 Cadence 문서를 읽고, 구현은 Temporal SDK로 옮기는 팀도 많습니다.
9. 실전: 주문 처리 워크플로우(Go 스타일)
아래는 주문 생성 → 재고 예약 → 결제 요청 → 결제 결과 시그널 대기 → 실패 시 재고 해제를 단순화한 예시입니다. 프로덕션에서는 트랜잭션 경계, 사기 탐지, 감사 로그, PG사별 API를 추가해야 합니다.
9.1 시그널·쿼리와 상태
// 개념 예시: 실제 프로젝트의 패키지 경로·클라이언트 생성은 환경에 맞게 조정합니다.
const (
SignalPaymentResult = "payment_result"
QueryOrderState = "order_state"
)
type PaymentResultPayload struct {
Success bool
TxID string
}
type OrderState string
const (
StatePending OrderState = "pending"
StateReserved OrderState = "inventory_reserved"
StatePaid OrderState = "paid"
StateCancelled OrderState = "cancelled"
StateFailed OrderState = "failed"
)
워크플로우가 노출하는 상태를 OrderState로 단순화하면, 쿼리 핸들러가 지원 화면에 바로 넘길 수 있습니다.
9.2 워크플로우 본문
import (
"fmt"
"time"
"go.uber.org/cadence"
"go.uber.org/cadence/workflow"
)
func OrderWorkflow(ctx workflow.Context, orderID string, amountCents int64) error {
logger := workflow.GetLogger(ctx)
state := StatePending
workflow.SetQueryHandler(ctx, QueryOrderState, func() (OrderState, error) {
return state, nil
})
paymentCh := workflow.GetSignalChannel(ctx, SignalPaymentResult)
var pay PaymentResultPayload
timerFired := false
ao := workflow.ActivityOptions{
StartToCloseTimeout: time.Minute,
RetryPolicy: &cadence.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2,
MaximumInterval: 30 * time.Second,
MaximumAttempts: 5,
},
}
actx := workflow.WithActivityOptions(ctx, ao)
if err := workflow.ExecuteActivity(actx, ReserveInventoryActivity, orderID).Get(actx, nil); err != nil {
logger.Error("reserve failed", "order", orderID, "err", err)
state = StateFailed
return err
}
state = StateReserved
if err := workflow.ExecuteActivity(actx, RequestPaymentActivity, orderID, amountCents).Get(actx, nil); err != nil {
_ = workflow.ExecuteActivity(actx, ReleaseInventoryActivity, orderID).Get(actx, nil)
state = StateCancelled
return err
}
sel := workflow.NewSelector(ctx)
sel.AddReceive(paymentCh, func(c workflow.Channel, more bool) {
c.Receive(ctx, &pay)
})
tf := workflow.NewTimer(ctx, 30*time.Minute)
sel.AddFuture(tf, func(f workflow.Future) {
timerFired = true
})
sel.Select(ctx)
if timerFired {
_ = workflow.ExecuteActivity(actx, ReleaseInventoryActivity, orderID).Get(actx, nil)
state = StateCancelled
return fmt.Errorf("payment timeout")
}
if !pay.Success {
_ = workflow.ExecuteActivity(actx, ReleaseInventoryActivity, orderID).Get(actx, nil)
state = StateCancelled
return fmt.Errorf("payment declined")
}
// 4) 결제 확정(매입 등)
if err := workflow.ExecuteActivity(actx, CapturePaymentActivity, orderID, pay.TxID).Get(actx, nil); err != nil {
state = StateFailed
return err
}
state = StatePaid
return nil
}
Selector로 시그널과 타이머를 함께 기다리면, “30분 내 결제 확정이 없으면 취소” 같은 정책을 코드로 명시할 수 있습니다. 타임아웃 분기에서는 재고 해제 Activity를 호출해 보상 트랜잭션을 수행합니다.
9.3 Activity 측(개념)
func ReserveInventoryActivity(ctx context.Context, orderID string) error {
// 재고 서비스 HTTP/gRPC — 멱등 키: orderID
return nil
}
func RequestPaymentActivity(ctx context.Context, orderID string, amountCents int64) error {
// PG에 결제 세션 생성 — 클라이언트는 웹훅에서 Workflow ID로 시그널 전송
return nil
}
func ReleaseInventoryActivity(ctx context.Context, orderID string) error {
return nil
}
func CapturePaymentActivity(ctx context.Context, orderID, txID string) error {
return nil
}
웹훅 핸들러는 주문 ID에서 Workflow ID를 매핑한 뒤 Cadence 클라이언트로 SignalWorkflow를 호출합니다. 웹훅은 서명 검증과 중복 전달에 대비해야 하며, 시그널이 두 번 와도 상태가 일관되도록 이벤트 ID 기반 중복 제거를 고려합니다.
9.4 워커와 Tasklist
// Worker 시작 시: OrderWorkflow를 등록하고 Tasklist "orders"를 구독하는 식으로 구성합니다.
// workflow.RegisterOptions{Name: "OrderWorkflow"} 와 함께 바인딩하는 패턴이 일반적입니다.
운영에서는 결제·재고·알림에 서로 다른 Activity 전용 워커를 두고 Tasklist를 나누면, 한 구간의 장애가 전체 폴링을 막는 것을 완화할 수 있습니다.
10. 모범 사례와 함정
- 워크플로우는 얇게: 긴 절차는 Child Workflow로 모듈화하고, 단계별 타임아웃·보상 트랜잭션을 분리합니다.
- 멱등성 우선: Activity는 “한 번만 실행된다”고 가정하지 않습니다. 주문·결제·재고 키를 외부 시스템 계약에 맞게 고정합니다.
- 시그널 순서: 동시 시그널이 올 때 비즈니스 규칙과 모순되지 않는지 테스트합니다.
- 비밀 정보: 카드 원번호 등은 워크플로우 인자에 넣지 않고 토큰·참조만 전달합니다.
- 관측성: 로그·메트릭에 Workflow ID, Run ID, Activity 타입을 구조화해 남깁니다.
11. 워커 폴링·Sticky Execution·히스토리 비용
11.1 폴링 루프와 동시성
워커는 Frontend/Matching 서비스에 long poll로 태스크를 요청합니다. 네트워크 지연·타임아웃이 잦으면 스케줄링 지연이 커지므로, 워커 프로세스의 CPU·스레드와 Activity 동시 실행 상한을 함께 튜닝합니다. 한 워커가 너무 많은 Activity를 동시에 잡으면 메모리·외부 API 연결이 병목이 됩니다.
11.2 Sticky Tasklist(개념)
일부 실행은 같은 워커 프로세스에 연속 태스크를 붙이는(sticky) 정책을 쓰기도 합니다. 세션형 캐시·네이티브 핸들을 워커 로컬에 두는 경우 워크플로우 본문이 아니라 Activity 측 최적화로만 다루어야 하며, 워커가 죽으면 다음 태스크는 다른 워커로 갈 수 있음을 전제로 멱등성을 유지합니다.
11.3 히스토리 크기와 리플레이 비용
이벤트가 수만 걸 넘어가면 워크플로우 태스크 한 번 처리하는 데 리플레이 비용이 커집니다. Continue-As-New, Child Workflow 분리, 타이머·시그널 설계 단순화로 히스토리 증가 속도를 줄입니다. 모니터링에는 히스토리 길이·워크플로우 태스크 백로그를 대시보드에 올리는 것이 좋습니다.
트러블슈팅
WorkflowExecutionAlreadyStarted → 동일 Workflow ID로 중복 시작을 시도했습니다. 멱등한 “시작” API는 Run ID를 반환받아 처리하거나, 비즈니스 키→Workflow ID 매핑을 유일하게 유지합니다.
Activity가 무한 재시도 → RetryPolicy의 NonRetriable 분류가 빠졌거나, 애플리케이션 에러가 재시도 가능으로만 보이는 경우입니다. 결제 거절·검증 실패는 즉시 실패로 올리고 보상 트랜잭션으로 넘깁니다.
워크플로우 태스크 타임아웃 / 결정론 오류 → 워크플로우 본문에 비결정론적 코드(랜덤, time.Now() 직접 호출, 숨은 I/O)가 들어갔을 가능성이 큽니다. 히스토리 리플레이 시 같은 분기가 나와야 하므로, 시각·랜덤은 워크플로우 전용 API로 치환합니다.
Tasklist 적체로 스케줄이 안 잡힘 → 워커 수·ScheduleToStart 타임아웃·호스트 리소스를 함께 봅니다. 한 Tasklist에 서로 다른 SLO 작업을 섞으면 헤드라인 블로킹처럼 보일 수 있어 큐 분리를 검토합니다.
시그널은 왔는데 상태가 안 바뀜 → 시그널 핸들러가 리플레이에서도 동일하게 상태를 갱신하는지, 쿼리 핸들러가 오래된 스냅샷만 보여 주는지 확인합니다.
12. 정리
Cadence는 이벤트 히스토리와 결정론적 워크플로우 코드를 결합해, 분산 환경에서도 업무 절차를 끊기지 않게 실행하는 플랫폼입니다. Domain으로 격리 경계를, Tasklist로 워커와 부하를 나누고, Workflow·Activity로 오케스트레이션과 부작용을 분리하며, 시그널·쿼리로 외부 세계와 운영 도구를 연결합니다. 재시도 정책은 외부 API의 현실과 멱등성 설계를 함께 봐야 하며, 워커 폴링·히스토리 크기는 운영에서 직접 체감되는 비용입니다. Temporal과의 관계를 이해한 뒤, 팀의 SDK·운영 조건에 맞는 스택을 선택하면 됩니다. 이 글의 주문 예제는 출발점으로 삼고, 실제 PG·재고·배송 연동과 장애 대응 런북을 조직 표준에 맞게 확장하기 바랍니다.
배포 전 git add, git commit, git push 후 npm run deploy를 실행하는 것이 안전합니다.
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「Cadence 완벽 가이드 — Uber 워크플로우 엔진」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「Cadence 완벽 가이드 — Uber 워크플로우 엔진」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Cadence로 분산 오케스트레이션을 구현하는 방법. Domain·Tasklist, Workflow·Activity, 시그널·쿼리, 재시도, Temporal 비교, 주문 처리 예제까지 정리합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
이 글에서 다루는 키워드 (관련 검색어)
Cadence, Workflow, Uber, Distributed Systems, Orchestration 등으로 검색하시면 이 글이 도움이 됩니다.