Claude Code Dynamic Workflow — 수백 개 서브에이전트로 대규모 마이그레이션·보안 감사를 자동화하는 법
올해 5월, Bun 런타임 창시자 Jarred Sumner가 X(트위터)에 짧은 글을 올렸습니다. 75만 줄의 Zig 코드를 Rust로 포팅하는 데 6일이 걸렸다고. 보통 이런 마이그레이션이라면 숙련된 팀이 수개월씩 매달리는 작업인데, 6일이라는 숫자가 믿기지 않아서 저도 한참 들여다봤습니다. 비결은 Anthropic이 Claude Code에 새로 추가한 Dynamic Workflow 기능이었습니다.
수천 개 파일에 걸친 분석이나 대규모 마이그레이션처럼 "규모의 문제"로 막혀 있었다면, 이 글이 도움이 될 거라 생각합니다. 프론트엔드 개발자든 DevOps 엔지니어든, 한 번에 다루기엔 너무 큰 코드베이스를 손대야 하는 상황이라면 실질적으로 쓸 수 있는 접근입니다. Dynamic Workflow는 단순히 "AI가 코드를 더 잘 짜준다"는 이야기가 아니라, 하나의 목표를 자동으로 수백 개의 하위 태스크로 쪼개고 이걸 병렬로 실행하는 서브에이전트 오케스트레이션 시스템입니다.
이 글에서는 Dynamic Workflow의 작동 원리부터 실무 적용 패턴, 그리고 예상치 못한 토큰 폭탄을 맞지 않는 법까지 솔직하게 정리해보겠습니다. 오케스트레이션이 처음이시라면 핵심 개념 섹션부터, 이미 개념을 아신다면 바로 '실전 적용'으로 건너뛰셔도 됩니다.
핵심 개념
JavaScript 오케스트레이션 스크립트가 컨텍스트를 대신 관리한다
기존 AI 에이전트 시스템 대부분은 모델이 "다음에 뭘 해야 할지"를 매번 추론하면서 작업을 이어갑니다. 그러다 보면 긴 작업일수록 컨텍스트 윈도우(AI 모델이 한 번에 처리할 수 있는 텍스트의 최대 한계)가 가득 차고, 중간에 맥락을 잃는 문제가 생기죠.
Dynamic Workflow는 접근이 다릅니다. 사용자가 자연어로 목표를 입력하면 Claude가 JavaScript 오케스트레이션 스크립트를 생성하고, Claude Code에 내장된 런타임이 그 스크립트를 백그라운드에서 실행합니다. 별도로 서버를 구성하거나 환경을 따로 세팅할 필요 없이, Claude Code v2.1.154 이상만 있으면 됩니다. 태스크 분해, 에이전트 수 결정, 반복 조건 같은 오케스트레이션 로직은 전부 스크립트 변수에 보존되기 때문에 모델의 컨텍스트 윈도우에는 최종 검증 결과만 들어옵니다.
사용자 입력 (자연어)
↓
Claude가 JS 오케스트레이션 스크립트 자동 생성
↓
Claude Code 내장 런타임에서 스크립트 실행
↓
수십~수백 개 서브에이전트로 병렬 분산
↓
검증된 결과만 최종 반환핵심 차이: 오케스트레이션 상태가 AI 모델의 컨텍스트 안이 아니라 스크립트 변수에 보존되기 때문에, 작업 규모가 커져도 컨텍스트 윈도우 고갈이 발생하지 않습니다.
세 가지 핵심 프리미티브
Dynamic Workflow 스크립트는 세 가지 기본 함수를 중심으로 구성됩니다.
| 함수 | 역할 | 특징 |
|---|---|---|
agent() |
단일 서브에이전트 실행 | JSON Schema 지정 시 구조화 출력 반환 |
pipeline() |
순차적 스트리밍 처리 | 스테이지 체인, 베리어 없음, 기본 권장 |
parallel() |
모든 태스크 동시 실행 | 베리어 방식, 전체 완료 대기 |
베리어 방식이란?
parallel()은 모든 에이전트가 완료될 때까지 다음 단계로 넘어가지 않습니다. 반면pipeline()은 앞 스테이지 결과가 나오는 즉시 다음 스테이지로 흘려보내기 때문에 전체 대기 없이 스트리밍됩니다.
pipeline()이 기본 권장인 이유는 앞 스테이지 결과가 나오는 즉시 다음 스테이지로 흘려보낼 수 있어서 parallel()보다 실제 처리 속도가 더 빠른 경우가 많기 때문입니다. 저도 처음엔 "병렬이 더 빠르지 않나?" 했는데, 베리어 없는 스트리밍이 주는 이점이 생각보다 큰 차이를 만들더라고요.
여섯 가지 공식 패턴
Anthropic이 공식 문서화한 구성 패턴 여섯 가지입니다. 실무에서 어떤 상황에 쓰면 좋을지 구체적인 예시와 함께 정리해봤습니다.
| 패턴 | 설명 | 적합한 상황 | 예시 |
|---|---|---|---|
| Classify-and-act | 입력을 분류 후 맞는 에이전트로 라우팅 | 다양한 유형의 이슈 트리아지 | GitHub 이슈 1,000건을 버그/기능/문의로 분류 후 담당 에이전트에 배분 |
| Fan-out-and-synthesize | 분산 처리 후 결과 종합 | 대규모 코드베이스 분석 | 모듈별 의존성 분석 → 전체 아키텍처 맵 생성 |
| Adversarial verification | 독립 검증자가 결과 반박 시도 | 높은 정확도가 요구되는 작업 | 보안 스캔 결과를 별도 에이전트가 오탐 여부 검토 |
| Generate-and-filter | 다수 생성 후 필터링 | 코드 제안, 테스트 케이스 생성 | 함수 구현 5가지 생성 후 성능 기준으로 상위 1개 선택 |
| Tournament | 여러 후보를 경쟁 평가 | 최적 구현 선택 | 알고리즘 3가지 구현 후 벤치마크로 우승자 결정 |
| Loop-until-done | 새 발견이 없을 때까지 반복 | 버그 탐색, 보안 감사 | 타입 에러 수정 → 재검사 → 에러 없을 때까지 반복 |
실전 적용
세 가지 시나리오를 골랐습니다. 각각 서로 다른 패턴을 조합하고 있어서, 코드를 살펴보다 보면 패턴들이 실제로 어떻게 맞물리는지 감이 잡히실 거라 생각합니다. 가장 임팩트 있는 케이스는 첫 번째 보안 감사인데, 단일 에이전트로는 컨텍스트 한계에 막혀서 제대로 수행하기 어려웠던 작업이거든요.
예시 1: 전체 저장소 보안 감사
처음 이 패턴을 써봤을 때 솔직히 놀랐습니다. 수천 개 파일을 그냥 훑는 게 아니라, 스캔 에이전트와 검증 에이전트를 분리해서 오탐률을 낮추는 구조가 꽤 영리하더라고요. Fan-out-and-synthesize와 Adversarial verification을 조합한 패턴입니다.
export const meta = {
name: 'security-audit',
description: '전체 저장소 보안 감사',
phases: [{ title: 'Scan' }, { title: 'Verify' }],
}
// 실제 사용 시 프로젝트에 맞게 정의해두시면 됩니다
const FINDING_SCHEMA = {
type: 'object',
properties: {
file: { type: 'string' },
severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },
title: { type: 'string' },
description: { type: 'string' },
},
required: ['file', 'severity', 'title', 'description'],
}
const VERDICT_SCHEMA = {
type: 'object',
properties: {
confirmed: { type: 'boolean' },
reason: { type: 'string' },
},
required: ['confirmed', 'reason'],
}
// 분석 대상 모듈 경로 배열
const MODULES = ['src/auth', 'src/api', 'src/db', 'src/middleware']
// 1단계: 모듈별로 스캔 결과 수집 (pipeline으로 스트리밍)
const findings = await pipeline(
MODULES,
m => agent(
`${m} 모듈의 인증 취약점, 입력 검증 누락, 안전하지 않은 패턴을 탐지하세요`,
{ phase: 'Scan', schema: FINDING_SCHEMA }
)
)
// 2단계: 발견된 취약점마다 독립 검증자가 반박 시도
const verdicts = await parallel(
findings.map(f => () =>
agent(
`다음 취약점 보고의 오탐 여부를 검토하세요: ${f.title} — ${f.description}`,
{ phase: 'Verify', schema: VERDICT_SCHEMA }
)
)
)
// 검증을 통과한 항목만 최종 보고
return findings.filter((f, i) => verdicts[i].confirmed)이 패턴의 핵심은 스캔과 검증을 분리한다는 점입니다. 첫 번째 에이전트 집합이 의심스러운 패턴을 찾아내면, 두 번째 에이전트 집합이 각각 독립적으로 "이게 진짜 취약점인가?"를 반박하려 시도합니다. 이렇게 하면 오탐률이 크게 낮아집니다.
실행 방법: 이 파일을
security-audit.js로 저장한 뒤, Claude Code에서/workflow run security-audit.js로 실행하거나, 자연어로 보안 감사를 요청하면 Claude가 유사한 스크립트를 자동 생성해줍니다.
예시 2: 대규모 코드 마이그레이션
앞서 언급한 Zig → Rust 포팅 작업처럼, 언어나 프레임워크 마이그레이션에도 잘 맞습니다. 파일 간 의존성 때문에 순서가 중요한 작업인데, 그냥 parallel로 던지면 안 되고 의존성 그래프를 먼저 분석해야 한다는 걸 처음엔 간과하기 쉽거든요. Loop-until-done 패턴으로 변환 후 검증까지 자동화합니다.
export const meta = {
name: 'framework-migration',
description: 'Express → Hono 마이그레이션',
phases: [{ title: 'Analyze' }, { title: 'Convert' }, { title: 'Validate' }],
}
// 마이그레이션 대상 파일 목록
const SOURCE_FILES = ['src/routes/auth.js', 'src/routes/user.js', /* ... */]
// FILE_ANALYSIS_SCHEMA: { path, deps, expressPatterns } 형태로 정의
// 1단계: 파일별 의존성 분석 (병렬)
const fileMap = await parallel(
SOURCE_FILES.map(f => () =>
agent(`${f}의 Express API 패턴과 미들웨어 의존성을 분석하세요`, {
phase: 'Analyze',
schema: FILE_ANALYSIS_SCHEMA,
})
)
)
// topologicalSort: 파일 의존성 그래프를 위상 정렬
// (순환 의존성 없이 변환 순서를 결정 — A가 B를 import하면 B를 먼저 변환)
const converted = await pipeline(
topologicalSort(fileMap),
f => agent(`${f.path}를 Hono 패턴으로 변환하세요. 의존 파일: ${f.deps}`, {
phase: 'Convert',
})
)
// ERROR_SCHEMA: { file, message, line } 형태로 정의
// 3단계: 변환된 파일 타입 체크 및 오류 수정 (Loop-until-done)
let iterations = 0
while (iterations < 5) {
const errors = await agent('변환된 코드의 타입 에러와 런타임 오류를 탐지하세요', {
phase: 'Validate',
schema: ERROR_SCHEMA,
})
if (errors.length === 0) break
await parallel(errors.map(e => () => agent(`${e.file}의 오류 수정: ${e.message}`)))
iterations++
}Loop-until-done 패턴 주의사항: 무한 루프 방지를 위해 반드시 최대 반복 횟수(
iterations < 5같은 가드)를 설정해두는 것이 좋습니다. 실무에서 자주 맞닥뜨리는 상황인데, 이걸 빠트리면 예상치 못한 토큰 소진으로 이어질 수 있습니다.
예시 3: 성능 최적화 탐색
프로파일러 결과를 기반으로 전체 코드베이스에서 최적화 포인트를 찾아내는 작업입니다. Tournament 패턴을 활용하면 여러 최적화 방안을 경쟁시켜 최선의 결과를 선택할 수 있습니다. 이건 제가 "이래서 단순 채팅으로는 안 되는 작업이 있구나"를 실감했던 케이스입니다 — 핫스팟 20개를 각각 3가지 방안으로 비교하면 60개 평가가 한 번에 병렬로 돌아가거든요.
export const meta = {
name: 'perf-optimization',
description: '프로파일러 기반 성능 최적화',
phases: [{ title: 'Identify' }, { title: 'Propose' }, { title: 'Rank' }],
}
// HOTSPOT_SCHEMA: { location, avgMs, callCount } 형태
const hotspots = await agent(
'프로파일러 결과에서 상위 20개 핫스팟을 추출하세요',
{ phase: 'Identify', schema: HOTSPOT_SCHEMA }
)
// PROPOSAL_SCHEMA: { description, estimatedGain, riskLevel } 형태
// 각 핫스팟에 대해 3가지 최적화 방안 생성 (Tournament 패턴)
const proposals = await parallel(
hotspots.map(h => () =>
agent(`${h.location}에 대한 최적화 방안 3가지를 제안하세요`, {
phase: 'Propose',
schema: PROPOSAL_SCHEMA,
})
)
)
// RANK_SCHEMA: { score, pros, cons } 형태
// 각 방안을 독립적으로 평가해 최고 순위 선택
const ranked = await parallel(
proposals.flat().map(p => () =>
agent(`이 최적화 방안의 예상 효과와 위험도를 평가하세요: ${p.description}`, {
phase: 'Rank',
schema: RANK_SCHEMA,
})
)
)
return proposals.flat()
.map((p, i) => ({ ...p, score: ranked[i].score }))
.sort((a, b) => b.score - a.score)
.slice(0, 5) // 상위 5개 추천장단점 분석
솔직히 이 기능 처음 쓸 때 장점만 보고 큰 작업을 무작정 던졌다가 토큰 청구서 보고 놀란 적이 있습니다. 아래 표를 보시기 전에, 특히 단점 섹션의 비용 항목은 꼭 먼저 읽어보시길 권합니다.
장점
| 항목 | 내용 |
|---|---|
| 규모 확장성 | 단일 세션에서 최대 1,000개 에이전트, 최대 16개 동시 실행 |
| 컨텍스트 독립성 | 오케스트레이션 로직이 스크립트 변수에 보존되어 컨텍스트 윈도우 고갈 없음 |
| 재개 가능성 | 모든 agent() 호출을 저널링하여 중단된 실행을 이어받아 재개 가능 |
| 적응적 설계 | 조건 분기, 반복, 에이전트 수 동적 결정 등 복잡한 제어 흐름 지원 |
| 검증 내장 | Adversarial verification 패턴으로 허위 결과 필터링 가능 |
| 성능 향상 | Opus 4 리드 에이전트 + Sonnet 4 서브에이전트 구성 시 단일 Opus 4 대비 90.2% 성능 향상 (Anthropic 내부 평가) |
단점 및 주의사항
나머지 항목들은 설계 시 미리 고려하면 피할 수 있지만, 토큰 비용은 한 번 터지고 나서 깨닫게 되는 경우가 많아서 가장 위에 뒀습니다.
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 토큰 비용 폭증 | Max 플랜($200/월) 기준 하루 실행으로 주간 한도 20% 소진 사례 보고 | 작업 전 에이전트 수 예상 산정, 작은 범위로 테스트 먼저 |
| 비결정성 금지 | Date.now(), Math.random(), 인수 없는 new Date() 스크립트 내 사용 불가 |
고정값 또는 외부 입력으로 대체 |
| 동시 실행 제한 | 최대 16개 동시 에이전트, 총 1,000개 한도 | 작업 배치 설계 시 한도 고려 |
| 단순 태스크 비효율 | 예측 가능한 단순 반복 작업에는 오버헤드 | 커스텀 서브에이전트 또는 단순 API 호출로 대체 |
시작하기 전 체크리스트
운영 측면에서 미리 확인해두면 좋은 항목들입니다.
- 버전 확인:
claude --version으로 v2.1.154 이상인지 확인해볼 수 있습니다. - 플랜 확인: Max·Team·Enterprise 플랜에서 사용 가능합니다. 해당 플랜이 없다면 Claude API를 키 기반으로 직접 사용하는 방법도 있으며, Amazon Bedrock, Google Vertex AI, Microsoft Foundry를 통한 접근도 지원됩니다.
- Enterprise 활성화: Enterprise 환경에서는 Dynamic Workflow가 기본 비활성화 상태입니다. 관리자가 명시적으로 활성화해야 사용 가능합니다.
- Research Preview 주의: 현재 연구 프리뷰 단계로, GA(정식 출시) 전에 API와 동작 방식이 변경될 수 있습니다. 프로덕션 크리티컬 파이프라인보다는 내부 도구나 자동화 스크립트부터 적용해보시는 게 안전합니다.
실무에서 가장 흔한 실수
- 에이전트 수 추정 없이 무작정 실행 — 100개 파일 × 3개 에이전트 × 2 패스를 계산해보지 않고 실행했다가 토큰이 예상의 10배로 나오는 경우가 있습니다. 처음엔 10개 파일로 드라이런을 해보시면 실제 소비량을 가늠해볼 수 있습니다.
parallel()과용 — "동시 실행이면 다parallel()이지"라는 생각으로 모든 곳에 쓰면 불필요하게 전체 완료를 기다리는 베리어가 생깁니다. 앞 결과를 바로 다음에 흘려보낼 수 있는 경우라면pipeline()이 더 효율적입니다.- 스크립트 내
Math.random()사용 — 재개 캐시가 무효화되어 중단된 작업을 이어받지 못하게 됩니다. 랜덤 시드가 필요하다면 외부에서 값을 받아 고정 인자로 넘기는 방식을 활용할 수 있습니다.
마치며
Dynamic Workflow는 "AI가 더 똑똑해진다"가 아니라, 기존 AI의 병목이었던 컨텍스트 한계를 아키텍처 수준에서 우회하는 접근입니다. 이미 단일 에이전트로 잘 되는 작업이라면 굳이 도입할 필요가 없지만, 수천 개 파일에 걸친 분석이나 대규모 마이그레이션처럼 "규모의 문제"로 막혀 있던 작업이라면 한번 시도해볼 만한 가치가 있습니다.
지금 바로 시작해볼 수 있는 3단계:
- 버전과 플랜을 확인해볼 수 있습니다.
claude --version으로 v2.1.154 이상인지 확인하고, Max·Team·Enterprise 플랜 여부를 체크해볼 수 있습니다. 플랜이 없다면 Claude API 키 기반으로도 접근 가능합니다. 참고로 Claude Code에서/effort ultracode를 입력하면 Ultracode 모드가 활성화되어, 이후 요청하는 모든 실질적 태스크에 대해 Claude가 자동으로 워크플로를 기획하고 실행하게 됩니다. - 작은 범위로 드라이런을 먼저 진행해보시면 좋습니다. 실제 프로젝트에서 모듈 하나, 파일 10개 정도를 대상으로 보안 감사나 리팩터링 태스크를 실행해보고 생성된 오케스트레이션 스크립트와 토큰 소비량을 확인해보시면 전체 규모로 확장하기 전에 비용 예측이 훨씬 쉬워집니다.
- Adversarial verification 패턴 하나를 직접 작성해보시면 이해가 빨라집니다. 이 패턴이 Dynamic Workflow에서 가장 차별화된 부분입니다. 스캔 에이전트가 찾은 결과를 검증 에이전트가 반박하는 구조를 한번 만들어보시면 전체 시스템의 설계 철학이 손에 잡히실 거라 생각합니다.
참고 자료
- Orchestrate subagents at scale with dynamic workflows | Claude Code 공식 문서
- Introducing dynamic workflows in Claude Code | Anthropic 블로그
- Claude Code Adds Dynamic Workflows for Parallel Agent Coordination | InfoQ
- How we built our multi-agent research system | Anthropic Engineering
- Multiagent sessions | Claude API 공식 문서
- Claude Code Dynamic Workflows: Orchestrating Agents at Scale | Context Studios
- Claude Code Dynamic Workflows Complete Guide | alexop.dev