01. Pipeline Pattern — 파이프 & 필터, batch vs stream, 오케스트레이션 vs 코레오그래피
이 문서가 답하는 질문: 미디어 변환을 어떤 패턴으로 단계화하는가? 단계 사이를 누가 지휘하는가?
한 줄 답
미디어 파이프라인은 파이프 & 필터(Pipes and Filters) 패턴이 기본형이고, 데이터 도착 양식에 따라 batch / stream으로 갈라지며, 단계의 의사결정을 누가 쥐느냐에 따라 orchestration / choreography로 갈라진다. 우리가 다루는 미디어 변환은 거의 “chunked-batch + choreography(EBus 라우팅) + 부분 orchestration(분기)” 의 hybrid다.
Why — 왜 모놀리식 람다 하나가 망하는가
가장 흔한 출발점은 한 람다에 모든 단계를 넣는 모놀리식이다.
이 구조의 4가지 무너지는 이유:
| 압력 | 무너지는 방식 | 일반 원칙 |
|---|---|---|
| 시간 | 람다 timeout(15분) 1시간 영상 transcode 불가 | ”단계의 시간 분포는 단계마다 다르다” |
| CPU/메모리 프로파일 | probe(저메모리) + transcode(고메모리)가 한 람다 메모리 설정 공유 | ”프로파일이 다르면 분리가 더 싸다” |
| 재시도 단가 | sprite 단계만 실패해도 transcode 재실행 → 비용 N배 | ”실패 단위 = 재실행 단위” |
| 관측 | ”어디서 실패?” 단일 stack trace로는 stage 식별 어려움 | ”단계는 1급 시민이어야 한다” |
→ 4가지 압력 중 어느 하나라도 들어오면 단계 분리(파이프 & 필터) 로의 전환이 강제된다.
How — 파이프 & 필터의 원리
1) 골격 — Filter는 stateless 변환자, Pipe는 전달자
Filter의 5계명:
- 단일 책임 — 한 Filter는 한 변환만 한다 (probe만, transcode만).
- Stateless — 입력 → 출력만 정의. 인스턴스 간 상태 공유 금지.
- 멱등 — 같은 입력 두 번 들어오면 같은 결과 (자세히는
04). - 명시적 입출력 스키마 — Pipe로 흘러가는 메시지의 모양은 contract.
- 자기 단계만 분류 — 자기가 throw하는 에러를 자기가 분류 (
05).
Pipe의 3계명:
- At-least-once 전달 — 중복은 허용, 손실은 금지 (자세히는
02). - 순서 보장은 옵션 — 필요할 때만 FIFO; 일반은 unordered.
- Backpressure 가능 — 소비자 속도에 맞춰 생산을 막을 수 있어야 함.
2) 데이터 도착 양식 — batch vs stream
| Batch | Stream | |
|---|---|---|
| 단위 | 파일 1개 (또는 묶음) | 끊임없는 byte/event 흐름 |
| 시간성 | 도착 후 처리 (eventual) | 연속 처리 (low-latency) |
| 예 | 업로드된 비디오 transcode | 라이브 스트리밍 인코딩 |
| 실패 단위 | 파일 단위 retry | 시점/오프셋 단위 retry |
| 인프라 | SQS + Lambda / Step Functions | Kinesis / Kafka / Flink |
미디어 변환은 거의 batch다 — 사용자가 업로드하고, 시스템이 비동기 처리하고, 결과 URL을 알린다. stream이 필요한 경우는 라이브 방송, 실시간 STT/번역 같은 좁은 영역.
chunked-batch — 큰 batch를 잘게 잘라 stream처럼 흘리는 hybrid가 흔하다. 예: 1시간 영상을 segment 단위로 쪼개 병렬 transcode (MediaConvert가 내부에서 함).
3) 의사결정 위치 — Orchestration vs Choreography
이 두 패턴은 “다음 단계로 갈지 말지를 누가 결정하는가” 의 차이다.
| 비교 축 | Orchestration | Choreography |
|---|---|---|
| 흐름 가시성 | 한 화면에 (Step Functions 그래프) | 분산 (어디로 가는지 추적 어려움) |
| 결합도 | 지휘자가 모든 단계를 안다 | 단계는 자기 다음만 publish |
| 분기 로직 | 지휘자가 if/else 가짐 | 이벤트 type/filter로 분기 |
| 수정 비용 | 지휘자만 수정 | 다수 subscriber 수정 가능 |
| 에러 처리 | 지휘자가 catch + 재시도 정책 | 각 단계가 자기 retry, DLQ |
| 관측 | 단일 trace ID 자연스러움 | 명시적 correlation ID 필요 |
| 벤더 | Step Functions, Airflow, Temporal | EventBridge, Kafka, Pub/Sub |
→ 둘은 배타적이지 않다. 고정 분기는 orchestration, 확장 가능 분기는 choreography가 어울린다.
What — 구체 사양과 패턴 카탈로그
1) 4가지 변형 패턴
A. Linear Pipeline (가장 단순)
[A] → [B] → [C] → [D]- 미디어 분석 → transcode → DB update 같은 일자형
- 단점: 하나 늦어지면 전체 늦어짐 (head-of-line blocking)
B. Branching (분기)
[A] → [B] ─→ [C1]
└→ [C2]- 한 입력에서 여러 산출물 생성 (HLS + sprite + waveform)
- 자세히는
03-fan-out-fan-in.md
C. Joining (병합)
[B1] ─┐
[B2] ─┴→ [C]- 여러 산출물이 모두 완료되어야 다음 단계 진행
- 동기화가 까다롭다 (
03참조)
D. Cyclic / Conditional Retry
[A] → [B] → [C]
↑ ↓
└─────┘ (조건부 재시도)- 일시 에러 시 같은 단계 재실행 — Step Functions의
Retry/Catch
2) 메시지 스키마 — Filter 사이 contract
각 Pipe를 흘러가는 메시지는 명시적 스키마여야 한다. 흔한 형태:
// 모든 단계 공통 필드
interface PipelineEvent {
// 1. 식별
pipelineId: string; // 한 파이프라인 실행의 trace
fileId: string; // 처리 대상
// 2. 멱등 — 04 참조
idempotencyKey: string; // 보통 contentHash 또는 (fileId + stage)
// 3. 시간 추적
enqueuedAt: ISO8601; // 큐 진입 시각
attemptNumber: number; // 재시도 횟수
// 4. 단계별 페이로드
stage: 'probe' | 'transcode' | 'sprite' | 'db-update';
payload: unknown; // stage별 schema
}스키마는 다음을 만족해야 한다:
- Forward compatible — 새 필드 추가 시 기존 consumer 안 깨짐
- Versioned — major 변경 시
version: 2로 분리 (또는 새 큐로 분리) - Validated at boundary — 메시지를 받자마자 스키마 검증, 실패 시 명시적 분류 (
05)
3) “단계는 어디까지 쪼개야 하나”의 기준
너무 잘게 쪼개면 메시지 hop 비용 + 복잡도 폭주. 너무 굵으면 모놀리식 회귀.
| 분리해야 하는 신호 | 합쳐도 되는 신호 |
|---|---|
| 시간 프로파일이 N배 차이 (probe 1초 vs transcode 10분) | 합쳐 1분 안에 끝남 |
| 메모리/CPU 프로파일이 다름 | 같은 메모리 설정으로 충분 |
| 의존 외부 시스템이 다름 (S3-only vs MediaConvert vs DB) | 같은 외부 의존만 사용 |
| 실패 단위를 따로 가져가고 싶음 | 한 단위로 같이 실패하는 게 자연스러움 |
| 다른 팀/모듈이 소유 | 한 팀이 소유 |
→ 한 영상 변환 사례: probe+loudness+waveform은 한 람다, sprite는 다른 람다, DB update는 세 번째 람다. transcode는 람다가 아니라 MediaConvert. 4단계 = 의존 외부의 갯수.
What-if — 잘못 쓰면 어떻게 깨지는가
1) 단일 람다 모놀리식의 함정 (앞서 본)
15분 timeout · 단일 retry policy · 부분 산출물 폐기.
2) “Filter가 stateful”이 되는 사고
// 안티패턴 — 모듈 전역 변수로 상태 보관
let cache = new Map<string, Buffer>();
export async function handler(event) {
if (cache.has(event.fileId)) { /* ... */ }
// cold start 후 다른 인스턴스는 캐시 못 봄
}람다는 컨테이너 reuse가 보장되지 않는다. 캐시는 명시적 외부 저장(Redis, DynamoDB)으로.
3) “지휘자가 너무 똑똑해진다”
Step Functions 안에 비즈니스 로직을 넣기 시작하면 IaC가 비즈니스 코드처럼 되어 변경 비용 폭주. → 지휘자는 흐름만, Filter는 변환만.
4) “Pipe로 큰 페이로드를 직접 흘림”
SQS 256KB 한도, Kafka는 기본 1MB. 비디오 자체를 메시지에 넣으면 안 됨. → S3에 올리고 포인터(s3Key)만 메시지에 실어 보낸다 (“claim check pattern”).
5) “Choreography에서 누가 끝났는지 모름”
이벤트만 publish하면 “전체 파이프라인이 완료됐다”는 신호가 없다.
→ 종착 단계가 명시적으로 pipeline.completed 이벤트를 publish, 또는 별도 saga/state-tracker.
Insight — 흥미로운 이야기
“파이프 & 필터는 1973년 Doug McIlroy의 메모에서 시작했다”
Bell Labs의 Unix 그룹에서 McIlroy는 한 줄 메모를 썼다 — “We should have some ways of coupling programs like garden hose”. 다음 날 아침 Ken Thompson이
|연산자를 구현했다. 50년 뒤 우리가 SQS+Lambda로 짜는 미디어 파이프라인은 그 메모의 분산·영속·비동기 버전일 뿐이다. 본질은 같다 — 작은 도구를 표준 입출력으로 잇는다.
“orchestration이 부활한 이유”
2010년대 microservices 붐에서 choreography가 유행했다 (“우리는 모놀리식이 아니다, EBus만 있으면 된다”). 그러나 5년 만에 다들 깨달았다 — 장애 디버깅이 너무 어렵다. 그래서 Temporal, Step Functions, Cadence 같은 orchestrator가 부활했다. 결론: 흐름 가시성이 비즈니스 critical할수록 orchestration 비중이 높아야 한다. 미디어 파이프라인은 사용자에게 “처리 중 X% / 실패” 같은 status를 보여줘야 하므로 orchestration 요소가 필수적.
“MediaConvert는 그 자체가 파이프라인 안의 파이프라인”
AWS MediaConvert는 한 Job 안에 다시 (분석 → split → 병렬 transcode → mux → manifest 생성)이라는 5단계가 들어있다. 우리가 짜는 파이프라인 안에 또 다른 매니지드 파이프라인이 들어가는 식. 추상화의 층이 생각보다 깊다.
요약 + Mermaid
요점 — 파이프 & 필터는 미디어 파이프라인의 골격이다. 단계는 시간·CPU·외부의존·실패단위·소유권 5축으로 쪼갠다. 흐름 의사결정은 orchestration(가시성)과 choreography(확장성) 사이의 trade-off를 hybrid로 푼다. 다음 문서(
02)는 이 골격에서 “Pipe”의 디테일을 본다.