죽지 않는 배포 파이프라인 — Netflix가 Temporal로 실패율을 40,000배 낮춘 이야기
배포 파이프라인은 도미노와 비슷하다. 수십 개의 조각이 순서대로 쓰러져야 마지막 조각까지 도달하는데, 중간에 하나라도 비틀거리면 전체가 멈춘다. Netflix의 배포 시스템이 정확히 이 상태였다. 네트워크 순단이나 클라우드 API 타임아웃 같은 일시적 장애 하나에 전체 배포의 4%가 실패하고 있었다. 며칠짜리 파이프라인이 중간에 끊기면? 처음부터 다시 도미노를 세워야 했다. Temporal이라는 Durable Execution 플랫폼을 도입한 뒤, 이 실패율은 **0.0001%**로 곤두박질쳤다.
Spinnaker — 배포의 심장이 아프다
Netflix의 멀티 클라우드 CD 플랫폼 Spinnaker는 두 핵심 컴포넌트로 구성된다.
- Orca: 파이프라인의 Stage와 Task를 오케스트레이션하는 지휘자다.
- Clouddriver: 실제 클라우드 인프라를 변경하는 실행자다.
전형적인 배포 흐름은 이렇다. 이미지 탐색 → 스모크 테스트 → 카나리 배포 → us-east-2 롤아웃 → 대기 → us-east-1 롤아웃. 클라우드 인프라를 건드리는 Stage에서 Orca가 Clouddriver에 POST 요청을 보내고, Clouddriver가 비동기로 작업을 수행하고, Orca가 폴링으로 진행 상태를 추적하는 구조였다.
문제는 "행복한 경로(happy path)"를 벗어나는 순간 터져 나왔다.
- 자체 오케스트레이션 부담: Clouddriver가 Orca와 별개로 내부 오케스트레이션을 운영해야 했다. 인프라 변경이라는 본업과 무관한 복잡성이 눈덩이처럼 불었다.
- 미로 같은 재시도 로직: 네트워크 장애, 클라우드 프로바이더 장애 등에 대응하느라 재시도 코드가 스파게티가 되었다.
- 자체 Saga 프레임워크: 중간 실패 시 이전 단계를 롤백하기 위해 자체 Saga를 만들어야 했다.
- 인스턴스에 묶인 상태: Clouddriver 인스턴스가 크래시하면 진행 중이던 작업 상태가 증발했다. Orca는 타임아웃까지 하염없이 기다릴 수밖에 없었다.
비유하면, 택배 기사가 배달뿐 아니라 경로 계획, 차량 정비, 사고 보고서 작성까지 혼자 다 하는 상황이다. 본업에 집중할 여력이 없다.
이 모든 방어막을 쌓고도 배포의 4%가 일시적 장애로 실패했다. 며칠짜리 파이프라인의 중간 실패는 전체 재실행을 의미하므로, 엔지니어링 생산성에 심각한 타격이었다.
[💡 잠깐! 이 용어는?] Durable Execution: 프로그램의 실행 상태를 영속적으로 저장해서, 장애/크래시/재시작이 발생해도 중단된 지점부터 재개할 수 있게 보장하는 플랫폼 특성이다.
Temporal은 무엇을 바꾸는가
Temporal은 비즈니스 로직을 두 가지로 나눈다. Workflow(결정론적 단계의 시퀀스)와 Activity(비결정론적 실제 작업). Worker 프로세스에서 Workflow가 실행되는 동안 Temporal 서버가 실행 상태를 영속 저장하므로, 장애가 나면 다른 Worker에서 이어서 실행할 수 있다.
@WorkflowInterface
public interface SleepForDaysWorkflow {
@WorkflowMethod
void run();
}
public class SleepForDaysWorkflowImpl implements SleepForDaysWorkflow {
private final SendEmailActivities emailActivities = Workflow.newActivityStub(
SendEmailActivities.class,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(10))
.build());
@Override
public void run() {
while (true) {
emailActivities.sendEmail();
Workflow.sleep(Duration.ofDays(30));
}
}
}
@ActivityInterface
public interface SendEmailActivities {
void sendEmail();
}이 코드에서 눈여겨볼 점이 네 가지다.
- Workflow와 Activity는 평범한 코드다. 기존 도구로 테스트할 수 있다.
- Activity는 설정 가능한 지수 백오프로 자동 재시도된다. 재시도 로직을 직접 짤 필요가 없다.
- Worker의 전원 케이블이 뽑혀도 Temporal이 다른 Worker에서 실행을 이어간다. 30일 sleep 도중이라도 마찬가지다.
Workflow.sleep은 프로세스를 점유하지 않는다. 컴퓨팅 자원을 소비하지 않는다.
[💡 잠깐! 이 용어는?] Activity의 멱등성(Idempotency): Temporal은 실패한 Activity를 자동으로 재시도한다. 따라서 같은 Activity가 여러 번 실행되어도 동일한 결과가 나오도록 설계해야 한다. 서버 생성 Activity라면, 이미 존재하는 서버를 확인하고 건너뛸 수 있어야 한다.
새 아키텍처 — Cloud Operation을 Workflow로 감싸다
@WorkflowInterface
interface UntypedCloudOperationRunner {
@WorkflowMethod
fun <OutputType : CloudOperationOutput> run(
stageContext: Map<String, Any?>,
operationType: String
): WorkflowResult<OutputType>
}interface CloudOperation<I : CloudOperationInput, O : CloudOperationOutput> {
@WorkflowMethod
fun operate(input: I, credentials: AccountCredentials<out Any>): O
}새로운 흐름은 자판기에 동전을 넣는 것처럼 단순해졌다.
- Orca가 Temporal Client를 통해
UntypedCloudOperationRunnerWorkflow 실행을 요청한다. - Clouddriver의 Temporal Worker가 작업을 받아
stageContext를 분석하고,operationType에 맞는CloudOperation구현체로 라우팅한다. - Child Workflow로 실제 Cloud Operation을 실행한다. 내부 Activity들이 클라우드 API를 호출한다.
- Orca는 Temporal Client로 Workflow 완료를 대기하고, 완료되면 결과를 받아 배포를 이어간다.
점진적 전환 전략
Orca에 CloudOperationRunner 인터페이스를 만들어 레거시 경로와 Temporal 경로를 캡슐화하고, Netflix의 동적 설정 시스템 Fast Properties로 런타임에 경로를 결정했다. Stage 타입, 클라우드 프로바이더 계정, 애플리케이션, Cloud Operation 타입별로 세밀하게 토글할 수 있었다. Spinnaker 서비스 자체를 먼저 Temporal로 배포한 뒤, 두 분기 만에 Netflix 전체 애플리케이션을 온보딩했다.
Before vs After
| 기준 | 기존 Clouddriver | Temporal 기반 |
|---|---|---|
| 배포 실패율 | ~4% | ~0.0001% |
| 상태 관리 | 인스턴스 로컬 (크래시 시 유실) | Temporal 서버 영속 저장 |
| 재시도 로직 | 자체 구현 (스파게티) | SDK 내장 (설정 기반) |
| 롤백 | 자체 Saga 프레임워크 | Temporal Workflow 통합 |
| Orca-Clouddriver 결합도 | 강결합 (직접 HTTP) | Temporal 중개 약결합 |
| 디버깅 | 로그 분석 | Temporal UI Workflow 시각화 |
| 인스턴스 성격 | 상태 보존 필요 (pet) | 상태 없음 (cattle) |
마이그레이션에서 얻은 교훈 세 가지
-
불필요한 Child Workflow를 피하라:
UntypedCloudOperationRunner가 Child Workflow를 시작해 실제 로직을 실행하는 구조는 간접 참조만 추가하고 트러블슈팅을 어렵게 만들었다. 클래스 합성으로 충분히 대체 가능했다. -
단일 인자 객체를 사용하라: Workflow와 Activity 함수에 여러 인자를 쓰면, 인자 추가/제거 시 Temporal의 결정론 제약 때문에 실행 중인 Workflow가 깨질 수 있다. 직렬화 가능한 단일 클래스로 인자를 감싸는 것이 권장 패턴이다.
-
비즈니스 실패와 Workflow 실패를 분리하라:
WorkflowResult타입으로 비즈니스 프로세스 실패(예: 배포 대상 서버 없음)와 Workflow 자체 실패(예: Temporal 서버 장애)를 구분했다. 에러 핸들링의 정밀도가 올라간다.
[💡 잠깐! 이 용어는?] 결정론 제약(Determinism Constraint): Temporal Workflow 코드는 같은 입력에 항상 같은 실행 경로를 보장해야 한다. 시간, 난수, 외부 호출 같은 비결정론적 요소는 반드시 Activity로 분리해야 한다. 이 제약 덕분에 Temporal이 Workflow를 안전하게 재개할 수 있다.
마무리
Netflix의 사례가 보여주는 핵심은 하나다. 직접 만든 오케스트레이션/재시도/상태 관리 코드를 전문 플랫폼으로 교체하면 극적인 안정성 향상을 얻을 수 있다. 4%에서 0.0001%로의 개선은 더 빠른 말을 만들려는 대신 자동차를 도입한 것과 같다. Activity 기본 재시도 타임아웃을 2시간으로 설정해두면, Clouddriver에 리그레션이 발생해도 고객 배포가 실패하기 전에 수정할 여유가 생긴다. 이것이 Durable Execution의 진정한 가치다.
관심 있을 만한 포스트
신입사원을 에이스로 — Netflix가 LLM Post-Training을 대규모 엔지니어링으로 만든 과정
Pre-training이 LLM에 넓은 언어 능력을 주지만, post-training이 실제 의도와 도메인 제약에 맞추는 단계. Netflix의 스케일링 접근법.
Temporal API — JavaScript Date의 30년 묵은 저주가 풀린다
Chrome 144가 Temporal API를 정식 탑재하면서 JavaScript 날짜 처리의 새 시대가 열렸다.
삽 대신 굴삭기 — Netflix가 400개 PostgreSQL 클러스터를 자동으로 옮긴 방법
Netflix Online Data Stores 팀이 400개에 가까운 RDS Postgres 클러스터를 Aurora Postgres로 자동 마이그레이션한 셀프서비스 워크플로우 설계 과정.
AI 코딩의 맹점 — Artifacts 없이 에이전트는 기억을 잃는다
PRD, ADR, TDD가 AI 코딩 워크플로우에서 왜 선택이 아닌 필수인지, 실전 구조와 함께 살펴본다.
Next-Translate 3.0 — Turbopack과 App Router를 위한 i18n 재건
1년간 공백 후 돌아온 Next-Translate 3.0이 Turbopack 지원, 비동기 params, App Router 안정화를 한 번에 처리하는 방법.
V8 WasmGC 투기적 최적화 — 가상 메서드를 인라인으로 만드는 법
V8이 WasmGC의 가상 메서드 디스패치에 투기적 인라이닝을 도입해 Dart와 Java 앱에서 최대 8% 성능을 끌어낸 방법.
Vinext — Vite 위에서 Next.js를 1주일 만에 다시 만든 이야기
Cloudflare가 AI와 함께 단 일주일, $1,100의 API 비용으로 Next.js 호환 프레임워크를 Vite 위에 구축한 과정.
Tsonic — TypeScript를 네이티브 바이너리로 컴파일하는 실험
TypeScript → C# → NativeAOT 파이프라인으로 네이티브 실행 파일을 만드는 Tsonic. 어떻게 동작하고, 어떤 한계가 있는지 살펴봤다.