팬아웃 마스터하기: 대규모에서의 영속적이고 멱등적인 워크플로 구축

SchemaBridge Team · 2025-12-15 · Scalability, Idempotency, Orchestration

데이터 손실 없이 1만 개 이상의 자식 작업을 처리합니다. Spawner 버텍스에 대한 심층 분석.

1만 개 항목의 도전: 루프가 죽어가는 곳

모든 개발자는 루프를 작성해 본 적이 있습니다. Java의 for 루프든, JavaScript의 .map()이든, Python의 list comprehension이든, 로직은 동일합니다: 항목 목록을 받아 각 항목에 대해 무언가를 수행하는 것입니다. 이는 데이터 처리의 가장 단순한 형태입니다. 10개 항목 규모에서는 사소한 일입니다. 100개 항목 규모에서는 관리 가능합니다. 하지만 수천 개, 결국 수백만 개의 문턱을 넘으면, 평범한 루프는 애플리케이션의 신뢰성과 확장성에 절대적인 죽음의 함정이 됩니다.

분산 시스템의 세계에서 로컬 루프는 장애 지점입니다. 10개 항목에서 1만 개 항목으로 이동하면, 복잡성은 단순히 선형적으로 증가하는 것이 아니라 복잡성의 벽에 부딪힙니다. 이 벽은 메모리 관리, 네트워크 지연, 그리고 여러분의 코드를 실행하는 머신의 필연적인 장애라는 냉혹한 현실로 만들어져 있습니다. 여러분은 더 이상 "로직"을 생각하지 않고 "물리학"과 씨름하게 됩니다.

로컬 루프의 한계: 왜 Promise.all은 스케일 이전 시대의 유물인가

단순한 구현에서는 거대한 JSON 페이로드—예를 들어 1만 건의 주문이 담긴 일일 CSV나 CRM에서 가져온 배치 내보내기—를 받아 다운스트림 서비스에 대한 API 호출을 루프로 감쌀 수 있습니다. 현대적인 JavaScript 개발자라면 Promise.all()을 사용해 모든 요청을 병렬로 실행할 수도 있습니다. 이것이 스케일 이전 개발자가 저지르는 첫 번째 실수입니다.

대규모에서는 이것이 세 가지 치명적인 이유로 재앙이 됩니다:

1. 메모리 고갈: 소리 없는 크래셔

1만 개의 복잡한 객체를 메모리에 로드하면 워커가 쉽게 크래시할 수 있습니다. 각 객체가 10KB에 불과하더라도 100MB의 원시 데이터를 다루게 되며, 메모리 사용이 많은 런타임에서는 500MB 이상으로 부풀어 오를 수 있습니다. 이는 첫 번째 항목이 처리되기도 전에 프로세스를 죽이는 즉각적인 "메모리 부족(OOM)" 오류입니다.

2. 실행 타임아웃: 시계가 째깍거리고 있다

대부분의 플랫폼에는 엄격한 제한이 있습니다. 루프가 1만 개의 API 호출을 수행하고 각 호출이 100ms만 걸린다 해도, 스크립트는 완료까지 거의 17분이 걸립니다. 병렬화하더라도 여전히 단일 컨테이너의 리소스 제한과 오버헤드에 묶여 있습니다. 마지막 항목이 처리되기도 전에 플랫폼에 의해 종료되어, 시스템을 불확정 상태로 남기게 됩니다.

Spawner 등장: 관리형, 영속적 팬아웃

SchemaBridge에서는 전용 Spawner 버텍스로 이 문제를 해결했습니다. Spawner는 단순한 루프가 아닙니다. 분산 오케스트레이션 기본 요소(Distributed Orchestration Primitive)입니다. 팬아웃을 그 자체로 관리되는 시스템으로 취급하며, 어떤 수의 워커로도 무리 없이 확장하도록 설계되었습니다.

Spawner의 실제 작동 방식: 병렬 분할

Spawner는 목록의 수집과 항목의 실행을 분리(decouple)합니다. 이는 부담을 여러분의 코드에서 우리의 인프라로 옮기는 중대한 아키텍처적 전환입니다:

1. 영속적 발행: 각 항목마다, SchemaBridge의 영속적 작업 큐에 고유한 "자식 워크플로" 이벤트를 발행합니다. 각 발행은 큐에 커밋되거나 실패하는 원자적 연산으로, 절반만 완료된 이벤트가 발생하지 않습니다.

2. 의도의 영속화: 각 발행은 부모 워크플로의 상태 이력(DynamoDB)에 기록됩니다. Spawner 노드가 루프 도중 죽으면, 새 노드가 이력을 참조해 정확히 마지막 항목부터 발행을 재개하여, 중복도 누락도 없도록 보장합니다.

3. 생애주기 독립성: 각 자식 항목은 엔진 내 일급 시민(first-class citizen)이 됩니다. 자체 ID, 자체 재시도 정책, 자체 로깅, 자체 상태를 갖습니다. 501번째 항목이 실패해도 502번째 항목의 성공을 막지 않습니다. 하나의 거대하고 취약한 배치 대신 1만 개의 개별 트랜잭션이라는 세밀함을 얻게 됩니다.

병렬성의 경제학: 관리형 스케일링이 비용을 절감하는 이유

15분 동안 무거운 단일 스레드 워커를 실행하는 것은 비용이 많이 듭니다. 고성능 메모리 인스턴스에 대한 비용을 지불할 뿐만 아니라, 코드가 API 응답을 기다리는 동안의 "유휴 시간"에 대해서도 비용을 지불하게 됩니다. 네트워크 I/O 대기 시간에 청구 가능한 CPU 사이클을 낭비하고 있는 것입니다.

SchemaBridge Spawner 모델에서는 수평적 효율성(Horizontal Efficiency)으로 전환합니다:

이 아키텍처는 전통적인 장기 실행 배치 스크립트에 비해 컴퓨팅 비용을 절감하는 동시에, 훨씬 더 높은 신뢰성과 100배의 관측 가능성을 제공합니다.

분산 팬아웃에서의 정확히 한 번(Exactly-Once) 시맨틱

팬아웃 패턴에서 가장 큰 장애물은 멱등성(Idempotency)입니다. Spawner 노드가 루프 도중 죽고 교체되면, 처음 1,000개 항목을 다시 발행하지 않도록 어떻게 보장할까요?

SchemaBridge는 영속적 상태 추적(Durable State Tracking)을 사용해 이를 처리합니다.

이는 개발자가 상태 확인 코드를 단 한 줄도 작성할 필요 없이 대규모에서 정확히 한 번(Exactly-Once) 처리를 제공합니다. 분산 일관성을 손쉽게 만든 것입니다.

포렌식 보고서: 100만 항목 장애

우리는 최근 100만 건의 원장 항목을 마이그레이션하는 핀테크 고객사와 협업했습니다. 그들은 전통적인 Python 스크립트를 사용하기로 했습니다. 12시간짜리 실행 도중, VPN 연결이 끊겼습니다. 스크립트가 크래시했습니다.

복구 (비싼 방식)

3명의 시니어 엔지니어가 복구하는 데 48시간이 걸렸습니다. 대상 데이터베이스를 스캔하고 수백 건의 레코드를 수동으로 대조해야 했습니다.

복구 (SchemaBridge 방식)

일주일 후, 그들은 SchemaBridge를 사용해 다시 100만 건을 실행했습니다. 동일한 VPN 연결 끊김이 발생했습니다.

1. 영속적 일시 정지: Spawner는 워커 큐에 도달할 수 없었기 때문에 단순히 발행을 멈췄습니다. "대기" 상태로 진입했습니다.

2. 자동 재개: VPN이 복구되자, Spawner는 내부 상태를 확인해 50만 번째 항목까지 완료했음을 확인하고, 즉시 50만 1번째 항목을 발행했습니다.

3. 인간의 가시성: 팀은 대시보드에서 진행률 표시줄이 실시간으로 재개되는 것을 지켜보았습니다. 단 한 줄의 코드도 작성되지 않았고, 단 하나의 데이터베이스 쿼리도 수동으로 실행되지 않았으며, 마이그레이션은 완벽하게 완료되었습니다.

상세 비교: 실제 사용되는 스케일링 모델

| 기능 | 단순 루프 (forEach) | SQS/Lambda (DIY) | SchemaBridge Spawner |

| :--- | :--- | :--- | :--- |

| 상태 관리 | 로컬 (휘발성) | 수동 (DB/큐) | 네이티브 (영속적) |

| 오류 처리 | 단일 try/catch | 수동 재시도/DLQ | 항목별 사가 |

| 가시성 | 로그 파일 조각 | 불투명한 큐 깊이 | 시각적 대시보드 |

| 조인 로직 | 어려움 (단일 스레드) | 매우 어려움 (카운터) | 네이티브 Merge 버텍스 |

| 멱등적 ID | 없음 | 수동 생성 | 자동 시퀀스 ID |

대용량 오케스트레이션을 위한 전문가 체크리스트

대용량 팬아웃을 설계할 때는, 저희 DevRel 팀이 권장하는 다음 원칙들을 따르세요:

1. 동시성을 엄격하게 정의하세요: 데이터베이스를 보호하기 위해 항상 max_concurrency 제한을 설정하세요. 낮게 시작(예: 10)해서 다운스트림 상태를 모니터링하며 늘려 나가세요.

2. 항목 실패를 정상으로 간주하세요: 목록의 각 항목이 독립적으로 재시도될 수 있도록 하세요. 항목별 사가를 사용해 부분 실패의 부작용을 정리하세요.

3. "롱테일" 지연을 모니터링하세요: 대시보드를 사용해 평균보다 10배 오래 걸리는 0.1%의 항목을 식별하세요. 이들은 보통 가장 복잡한 엣지 케이스이거나 데이터베이스 락 대상입니다.

4. 결정론적 ID를 활용하세요: 재시작에 대비하려면 항상 엔진에 내장된 시퀀스 ID를 사용하세요. 고동시성 팬아웃에서 고유성을 위해 타임스탬프에 의존하지 마세요.

결론: 스케일링은 인프라 문제다

팬아웃을 마스터한다는 것은 더 나은 루프를 작성하는 것이 아니라 분산을 위해 설계하는 것입니다. 반복, 발행, 영속화의 복잡성을 인프라로 옮김으로써, "부분 실패"의 위험을 제거하고 10개 항목을 다루듯 안전하게 수백만 개 항목을 다룰 수 있는 파이프라인을 구축할 수 있습니다.

2026년에는 스케일링이 더 이상 두려움이나 48시간짜리 포렌식 조사의 원천이어서는 안 됩니다. 이는 구성(configuration)의 해결된 문제여야 합니다. SchemaBridge는 불가능했던 루프를 가능하게 만들어, 어제의 기술 부채 없이 내일의 글로벌 규모 시스템을 구축할 수 있게 해줍니다.

4부에서는 "멱등성 엔진"을 다루며, 어떤 수의 병렬 브랜치에서도 분산 트랜잭션을 안전하게 유지하는 수학적 패턴을 탐구합니다. 해시 충돌 이론과 운영 오버헤드 없이 '정확히 한 번(Exactly-Once)'을 보장하는 방법을 살펴봅니다.

살펴보기