03. Fan-out / Fan-in — 분기·병합 패턴, 합류 시 동기화
이 문서가 답하는 질문: 한 입력에서 여러 산출물을 만들고 다시 합치려면 어떻게 하는가?
한 줄 답
Fan-out은 한 입력을 N개 분기로 흩뿌리는 것, fan-in은 N개 결과를 하나로 합치는 것이다. Fan-out은 쉽다 (이벤트 publish + 다수 subscriber). Fan-in이 어렵다 — N개가 모두 끝났다는 사실을 누가, 어디에서 알 것인가가 핵심. 도구 선택은 “분기 갯수가 정적이냐 동적이냐”로 갈린다 — 정적이면 EventBridge 라우팅, 동적이면 Step Functions Map.
Why — 왜 fan-out이 필요한가
미디어 한 파일은 보통 여러 산출물을 동시에 필요로 한다.
업로드된 비디오 1개 → HLS 매니페스트 + thumbnail sprite + waveform + metadata.json + DB row
└── HLS도 다시 360p / 720p / 1080p로 fan-out만약 이걸 linear pipeline으로 짜면:
문제:
- 순차 → 총 시간 = sum. 병렬이면 max로 단축 가능.
- 부분 실패가 전체 실패. sprite 실패가 waveform 실패를 만든다.
- 단계 의존성이 인위적. waveform은 transcode와 무관한데 왜 뒤에 있는가.
→ Fan-out으로 풀면 시간은 max, 부분 실패는 부분 손실로 격리.
How — Fan-out과 Fan-in의 실제 패턴
1) Fan-out 패턴 4가지
A. EventBridge Rule fan-out (정적, 선언적)
한 이벤트를 여러 룰이 매칭해서 각자 다른 큐/람다로 보냄.
- 분기 조건: 메시지 attribute(
detail-type,status등) - 새 분기 추가: 새 rule만 추가, 기존 코드 무변경
- 한계: 분기 갯수가 사전에 정해져 있어야 함 (정적)
B. SNS topic + multi-subscriber fan-out
같은 메시지를 여러 SQS/Lambda/HTTP가 동시에 받음.
- EventBridge보다 라우팅 능력 약함 (filter는 있지만 단순)
- 모든 subscriber가 같은 메시지 받음 (동일 페이로드)
- pre-EventBridge 시대의 패턴 — 지금은 EventBridge 선호
C. Step Functions Map (동적 분기)
런타임에 결정되는 N개 항목을 병렬 처리.
// Step Functions ASL
{
"Type": "Map",
"ItemsPath": "$.segments", // 동적으로 결정되는 배열
"MaxConcurrency": 10,
"Iterator": { "Type": "Task", "Resource": "...lambda..." }
}- 분기 갯수가 입력 데이터에 따라 다를 때 (예: 영상을 N segment로 쪼갬)
- 동시성 제한, 부분 실패 시 자동 retry, 완료 집계 모두 자동
- 단점: Step Functions state transition 비용
D. Producer가 직접 N개 publish (코드 fan-out)
const segments = await splitVideo(video);
await Promise.all(segments.map(s => sqs.send({ /* ... */ })));- 가장 단순, 동적 갯수 OK
- 부분 실패 시 어느 segment까지 publish됐는지 추적 필요 → 멱등성 + 재실행으로 풀어야 함
- 작은 N에 적합
2) Fan-in 패턴 — “모두 끝났다”를 누가 아는가
Fan-out은 쉽지만 fan-in은 항상 어렵다. 핵심 질문:
N개 단계 중 마지막 하나가 끝났음을 누가, 어디에서 알 것인가?
A. Counter pattern (DB / Redis)
각 단계가 끝날 때 카운터 +1, 카운터 == N이면 다음 단계 호출.
- 단순. 그러나 race condition 위험 — INCR 후 read 사이에 다른 INCR이 들어올 수 있음.
- atomic INCR + 결과값 체크로 풀어야 함:
INCR pipeline:abc:done → 결과가 N이면 호출 - DB 사용 시 transaction 필요.
B. Step Functions Parallel / Map 자동 join
{
"Type": "Parallel",
"Branches": [
{ "StartAt": "HLS", "States": { /* ... */ } },
{ "StartAt": "Sprite", "States": { /* ... */ } },
{ "StartAt": "Waveform", "States": { /* ... */ } }
],
"Next": "Aggregate"
}- Step Functions가 자동으로 모든 branch 완료를 기다린 뒤
Next로 진행 - 한 branch라도 실패하면 전체 실패 (catch로 부분 성공 허용 가능)
- 가장 깔끔하지만 SFN 비용
C. Saga pattern (코레오그래피 fan-in)
각 단계가 자기 완료 이벤트를 publish, 수집자(saga) 가 모든 이벤트를 보고 상태 추적.
- 각 단계가 끝나면
pipelineId, stage, status이벤트 발행 - Saga가 상태를 누적, “모두 done”이 되면 다음 트리거
- 유연하지만 saga 자체의 가용성/일관성 부담
D. “Eventually consistent” — fan-in을 안 함
각 단계가 자기 산출물을 자기가 DB에 기록. 사용자가 “모두 준비됐는지”를 polling하거나, frontend가 산출물 별 fallback 처리.
- 가장 간단. 미디어 파이프라인에서 흔함.
- 사용자에게 “0%, 33%, 66%, 100%” 같은 progressive 피드백 가능.
- “전체 완료” 알림이 정말 필요한 경우만 saga.
→ 흔한 영상 변환 사례는 D에 가깝다 — Lambda①·②가 각자 status 메시지를 보내고 Lambda③이 누적. 산출물 테이블 UPSERT로 자연스러운 fan-in.
What — 도구 매트릭스
1) Fan-out 도구 비교
| 도구 | 동적 갯수 | 라우팅 능력 | 비용 | 적합 |
|---|---|---|---|---|
| EventBridge | X (정적 룰) | 강력 (JSON 패턴) | 발행당 1M / $1 | 정적 분기, 다수 룰 |
| SNS topic | X | 약함 (filter policy) | 100M req / $0.5 | 단순 broadcast |
| Step Functions Map | O | 무관 | state transition당 | 동적 갯수, 동시성 제한 |
| 코드 fan-out | O | 직접 짜야 | publish 비용만 | 작은 N, 단순 |
2) Fan-in 도구 비교
| 패턴 | 일관성 | 복잡도 | 부분 실패 처리 | 적합 |
|---|---|---|---|---|
| Counter (Redis INCR) | strong (atomic) | 낮음 | 직접 짜야 | 작은 fan-out |
| Counter (DB row) | strong (transaction) | 중간 | DB와 결합 | DB 이미 있을 때 |
| Step Functions Parallel | strong | 낮음 | 자동 catch | 정적 N |
| Step Functions Map | strong | 낮음 | 자동 + retry | 동적 N |
| Saga / state tracker | eventual | 높음 | 직접 짜야 | 복잡한 워크플로 |
| Eventually consistent | eventual | 매우 낮음 | 자연스럽게 부분 성공 | 미디어 산출물 |
3) 미디어 파이프라인의 흔한 토폴로지
이 그림에서:
- Fan-out 1: probe → MC + waveform + loudness (병렬 시작)
- Fan-out 2: MC 안에서 360p/720p/1080p로 segment 분할 (MC 내부)
- Fan-out 3: EventBridge가 COMPLETE/ERROR로 라우팅
- Fan-in: 각 산출물이 status queue로 보내고, DB 갱신 lambda가 artifacts UPSERT로 누적
→ “전체 완료” 신호는 명시적으로 만들지 않는다. medias.status가 COMPLETE인지 + 필요한 artifacts가 다 있는지로 정의.
What-if — 잘못 쓰면 어떻게 깨지는가
1) “Counter pattern을 non-atomic하게 짬”
// 안티패턴 — race condition
const count = await db.read('counter');
await db.write('counter', count + 1);
if (count + 1 === N) trigger(); // 두 worker가 동시에 N번째일 수 있음→ atomic INCR 또는 DB transaction 필수. Redis는 INCR이 원자적, DynamoDB는 conditional update.
2) “Step Functions에 너무 많은 비즈니스 로직”
ASL JSON에 if/else/loop을 잔뜩 넣으면 IaC가 비즈니스 코드가 됨 → 변경 비용 폭주. → Step Functions는 흐름만, 로직은 Lambda 안에.
3) “Fan-out 후 한 분기 실패 = 전체 실패로 처리”
await Promise.all([hls(), sprite(), waveform()]);
// 하나라도 throw면 전체 throw미디어 파이프라인에서 sprite 하나 실패가 HLS까지 폐기시킬 이유가 없다.
→ Promise.allSettled() + 부분 산출물 허용 + 사용자에게는 “available + missing” 노출.
4) “EventBridge rule 너무 광범위”
{ "source": ["aws.mediaconvert"] } // 이 계정의 모든 MC 이벤트 매칭다른 팀의 MC 작업까지 받아 잘못된 큐에 들어감.
→ detail.queue 등으로 자기 큐만 필터링.
5) “Saga가 비대해짐”
Saga가 모든 단계의 모든 상태를 알게 되면 결국 모놀리식. → saga는 흐름 상태만, 데이터는 각 단계 소유.
6) “MaxConcurrency 안 잡고 Step Functions Map”
{ "Type": "Map", "ItemsPath": "$.items" } // 동시성 무제한10000개 segment가 동시에 Lambda 호출 → throttling 또는 다운스트림 폭발.
→ MaxConcurrency: 10 같은 제한 필수.
7) “Fan-in 시점에 데이터 일관성 가정”
병렬 단계가 끝난 직후에 DB에서 산출물 읽으려 하면 read replica lag 만날 수 있음. → 메시지 안에 산출물 포인터를 직접 실어 보내거나, primary read.
Insight — 흥미로운 이야기
“Map-Reduce는 fan-out/fan-in의 원형이다”
Google의 Map-Reduce(2004) 논문이 사실상 fan-out/fan-in의 정식화다 — Map 단계가 fan-out, Reduce 단계가 fan-in. 20년 뒤 Step Functions Map state는 이름 그대로 그 상속. Hadoop, Spark, Beam 모두 이 추상화의 후예. 미디어 파이프라인에서 한 파일을 N개 segment로 쪼개 병렬 transcode하는 것은 Map-Reduce 그 자체다.
“MediaConvert는 fan-out/fan-in을 외주받는 매니지드 서비스”
AWS MediaConvert가 비싼 이유 — 한 Job 안에서 자동으로 (a) 입력 분석, (b) segment 분할, (c) 병렬 워커에 분산, (d) 인코딩, (e) 결과 합쳐 manifest 작성을 한다. 이걸 직접 짜면 Step Functions + Lambda + S3로 큰 코드가 된다. minute당 비용이 비싼 만큼 fan-out/fan-in의 운영 부담을 통째로 가져간다.
“Choreography의 fan-in은 결국 ‘eventual consistency를 받아들이자’다”
정확한 fan-in을 코레오그래피로 구현하는 것은 saga + 카운터 + 트랜잭션 등 큰 인프라가 필요하다. 미디어 파이프라인 같은 사용자 시나리오는 eventual fan-in이면 충분 — 사용자가 화면을 새로고침하면 더 많은 산출물이 보이는 식. 강한 동기화는 정말 필요할 때만.
“Pinterest의 fan-out 사고 (2013)”
Pinterest가 follower fan-out을 했다 — 한 사람이 핀을 만들면 follower 1만 명의 timeline에 fan-out write. 한 명이 슈퍼스타가 되면 fan-out이 polynomial하게 폭발했다. 결국 hybrid로 전환 — 보통 사용자는 fan-out, 슈퍼스타는 read-time merge. 미디어 파이프라인도 마찬가지 — fan-out N이 무한히 커질 수 있으면 항상 limit 필요.
요약 + Mermaid
요점 — Fan-out은 쉽고 fan-in이 어렵다. 도구는 “분기가 정적/동적”으로 갈리고 (정적 → EventBridge, 동적 → Step Functions Map), fan-in은 “강한 동기 vs eventual”의 결정이다. 미디어 파이프라인은 대부분 eventual fan-in으로 충분 — 각 단계가 자기 산출물을 DB에 UPSERT하면 자연스러운 fan-in이 만들어진다. 다음 문서(
04)는 이 모든 것이 의존하는 멱등성을 본다.