Claude Agent SDK에서 서브에이전트 팬-아웃 설계와 부모 프로세스 결과 수집
에이전트를 여러 개 띄워 동시에 돌리면 처리량이 올라간다는 이야기는 익숙합니다. 다만 막상 코드를 앉히려 하면 서브에이전트를 어디에 어떻게 정의하고, 결과를 부모 프로세스로 어떻게 회수하며, 파일 충돌은 어떤 지점에서 어떻게 막아야 하는지가 흩어져 있어 전체 그림을 잡기 어렵습니다.
이 글은 Claude Agent SDK와 Claude Code CLI가 서브에이전트를 다루는 방식을 각각 구분해 정리하고, 팬-아웃/팬-인 패턴으로 병렬 실행을 설계하는 흐름을 짚어봅니다. 상용 API의 세부 시그니처는 계속 변경되고 있으므로, 아래 파이썬 코드는 모두 개념 흐름을 보여주기 위한 의사코드입니다. 실제 함수·클래스 이름과 시그니처는 Claude Agent SDK 공식 문서와 Claude Code Subagents 문서에서 확인하시는 편이 안전합니다(2026년 9월 시점).
개념 정리: 오케스트레이터와 서브에이전트
오케스트레이터(부모 프로세스)는 고수준 요구를 받아서 하위 작업으로 쪼개고, 서브에이전트에 위임한 뒤, 회수된 결과를 합성합니다. 서브에이전트는 오케스트레이터가 넘긴 하위 작업 하나에 집중하며, 다음 세 가지 특성을 가집니다.
- 독립 컨텍스트 윈도우: 부모의 긴 대화 이력이나 다른 형제 에이전트의 노이즈가 섞이지 않습니다.
- 개별 시스템 프롬프트: 에이전트별로 역할 지시를 다르게 줄 수 있습니다.
- 개별 툴 권한: 필요한 도구만 노출해 사고 반경을 좁힐 수 있습니다.
이 격리 특성 때문에, 부모 컨텍스트를 오염시키기 쉬운 대용량 탐색 작업(로그 훑기, 대규모 코드 스캔 등)을 서브에이전트로 밀어 넣는 전략이 유효합니다. 반대로 결과 요약이 계속 부모로 흘러들어오므로, 팬-아웃 규모가 커지면 부모 컨텍스트가 오히려 소진될 수 있다는 점이 뒷장에서 다룰 함정입니다.
두 갈래의 실행 환경 구분
서브에이전트를 다루는 경로는 크게 두 가지이고, 이 둘은 개념은 비슷하지만 실행 모델과 설정 방식이 다릅니다. 혼동하면 코드 예시가 어긋납니다.
| 구분 | Claude Code CLI | Claude Agent SDK(Python/TS) |
|---|---|---|
| 실행 환경 | 로컬 CLI 프로세스 | 사용자 애플리케이션 프로세스 |
| 서브에이전트 정의 | .claude/agents/*.md(YAML 프론트매터) |
SDK 옵션·설정 객체 |
| 위임 메커니즘 | 내장 Task Tool 호출 | SDK가 제공하는 위임 API |
| 병렬 처리 특성 | 오케스트레이터가 필요 시 Task를 여러 개 발주 | 애플리케이션 코드가 명시적으로 동시 실행을 스케줄링 |
즉 CLI에서는 오케스트레이터 프롬프트가 "이 작업들을 병렬로 위임하라"고 지시해야 Task Tool이 동시 발주하고, SDK 사용 코드에서는 애플리케이션이 직접 이벤트 루프에서 동시 호출을 띄웁니다. 두 방식 모두 서브에이전트 개별 컨텍스트라는 아이디어는 동일합니다.
Claude Code CLI에서 서브에이전트 정의
CLI 환경에서 서브에이전트는 프로젝트 내 .claude/agents/ 디렉터리에 마크다운 파일로 정의합니다. 파일 이름이 에이전트 식별자가 되고, YAML 프론트매터로 메타데이터를 지정합니다.
project/
├── .claude/
│ └── agents/
│ ├── code-reviewer.md
│ ├── security-checker.md
│ └── doc-writer.md
└── src/각 파일은 대략 다음 형태를 따릅니다. 사용 가능한 필드와 툴 이름은 CLI 버전에 따라 다르므로 공식 문서에서 확인하시길 권합니다.
---
name: code-reviewer
description: 코드 가독성과 패턴 위반을 중심으로 분석하는 리뷰어
tools:
- Read
- Grep
---
당신은 경험 많은 코드 리뷰어입니다.
주어진 파일을 읽고 가독성·유지보수성·잠재 버그를 지적하세요.
결과는 반드시 아래 JSON 스키마로만 반환합니다.
{ 'file': ..., 'issues': [{ 'severity': ..., 'message': ... }] }CLI에서 오케스트레이터에게 "각 모듈을 병렬로 리뷰하라"고 지시하면, Task Tool이 위 정의를 근거로 동일한 에이전트 유형의 인스턴스 여러 개를 발주하는 흐름이 됩니다.
Python SDK에서 팬-아웃 실행
애플리케이션 코드에서 서브에이전트 병렬 실행을 스케줄링할 때의 골격은 다음과 같습니다. 아래 코드는 실제 API 시그니처가 아니라, 흐름을 이해하기 위한 개념 예시입니다. 실제 함수명은 SDK 버전에 따라 다릅니다.
# 개념 예시 — 실제 API 시그니처는 공식 문서 참조
import asyncio
from typing import Any
async def run_parallel_reviews(modules: list[str]) -> dict[str, Any]:
async def review_one(module: str) -> dict[str, Any]:
# SDK가 제공하는 서브에이전트 실행 함수(개념)
return await run_subagent(
agent="code-reviewer",
prompt=(
f"{module} 모듈을 리뷰하고, "
"issues 배열을 포함한 JSON만 반환하세요."
),
)
results = await asyncio.gather(*(review_one(m) for m in modules))
return synthesize(results)
def synthesize(results: list[dict[str, Any]]) -> dict[str, Any]:
issues: list[dict[str, Any]] = []
for r in results:
issues.extend(r.get("issues", []))
return {"issue_count": len(issues), "issues": issues}포인트는 세 가지입니다. 첫째, 각 review_one 호출이 독립 컨텍스트를 가진 서브에이전트를 띄웁니다. 둘째, 결과 스키마를 JSON으로 강제해 부모에서 파싱 가능한 형태로 회수합니다. 셋째, 합성 로직은 코드로 처리해 LLM 호출을 한 단계 아낍니다.
CLI든 SDK든, 서브에이전트가 부모에 직접 응답하는 것이 아니라 위임 계층(Task Tool 또는 SDK 위임 API)이 결과를 취합해 부모에게 전달하는 구조라는 점이 실제 동작과 부합합니다.
파이프라인 안에서 순차와 병렬 섞기
실무 파이프라인은 순수 병렬보다 "공통 컨텍스트를 순차로 확보 → 독립 분석을 병렬로 → 결과를 순차로 합성"하는 혼합 구조가 더 흔합니다.
# 개념 예시 — 반환 타입은 코드에서 명시적으로 파싱
import json
async def mixed_pipeline(codebase_path: str) -> dict:
# 1단계: 순차 — 모듈 목록을 JSON으로 회수
raw = await run_agent(
agent="orchestrator",
prompt=(
f"{codebase_path}의 주요 모듈 목록을 "
'{"modules": [...]} 형태의 JSON만으로 반환하세요.'
),
)
modules: list[str] = json.loads(raw)["modules"]
# 2단계: 병렬 — 독립 모듈 분석
analysis = await asyncio.gather(*(
run_subagent(agent="code-reviewer", prompt=f"{m} 모듈 분석")
for m in modules
))
# 3단계: 순차 — 통합 리포트
return await run_agent(
agent="orchestrator",
prompt=f"다음 분석 결과를 통합 리포트로 정리하세요: {analysis}",
)반환값이 문자열일 수 있으므로 명시적으로 json.loads로 파싱한 뒤 인덱싱하는 편이 안전합니다. 서브에이전트에게 "JSON만 반환"을 요구하는 프롬프트 강제도 함께 쓰는 것이 실전에서 잘 맞습니다.
파일 충돌 방지: Git 워크트리 격리
여러 서브에이전트가 동일 파일을 동시에 수정하면 경쟁 조건이 발생합니다. 이를 완화하는 대표적인 방식이 에이전트별로 별도 Git 워크트리를 부여하는 것입니다. 개념적으로는 다음처럼, 실행 전에 워크트리를 만들어 서브에이전트에 작업 디렉터리로 넘겨줍니다.
# 개념 예시 — 실제 옵션 이름은 SDK/CLI 문서 확인
import subprocess, pathlib
def create_worktree(base_repo: str, branch: str, path: str) -> pathlib.Path:
subprocess.run(
["git", "-C", base_repo, "worktree", "add", path, branch],
check=True,
)
return pathlib.Path(path)
async def fix_in_isolation(module: str) -> dict:
wt = create_worktree("/repo", f"fix/{module}", f"/tmp/wt-{module}")
return await run_subagent(
agent="code-fixer",
prompt=f"{module} 모듈의 이슈를 이 워크트리에서 수정하세요.",
cwd=str(wt),
)Claude Code CLI에는 서브에이전트별 격리 옵션이 별도로 존재하므로, CLI 사용자라면 옵션 지원 여부를 문서에서 우선 확인하시고, 지원되지 않는 환경에서는 위처럼 애플리케이션 레이어에서 워크트리를 만들어 넘기는 방식으로 대체할 수 있습니다.
흔히 밟는 함정 정리
의존성 있는 작업의 병렬화: A의 출력이 B의 입력이라면 병렬이 아니라 순차입니다. 팬-아웃을 설계하기 전에 의존성 그래프를 그리고, 같은 레벨에는 서로의 결과를 몰라도 완수 가능한 작업만 배치하시는 편이 안전합니다.
모호한 서브에이전트 프롬프트: 서브에이전트는 부모의 대화 이력을 알지 못합니다. 부모 프롬프트에 담긴 배경을 서브에이전트 프롬프트에 자족적으로 다시 담아야 합니다. "이전 단계에서 정한 규칙에 따라"라는 표현은 서브에이전트에게는 공백입니다.
컨텍스트 역류: 격리를 위해 서브에이전트 수십 개를 띄웠는데, 결과 요약이 모두 부모로 돌아와 쌓이면서 부모 컨텍스트가 오히려 터지는 상황입니다. 결과를 구조화된 JSON으로 받고, 부모는 필요한 필드만 뽑아 상위 프롬프트에 다시 태우는 방식으로 완화됩니다.
사용자 질의 채널 부재: 서브에이전트는 실행 중 사람에게 되물어 확인을 받을 수 없습니다. 예를 들어 파괴적 수정(파일 삭제, 원격 푸시, 스키마 변경)에 대한 권한 승인을 필요로 하는 작업을 서브에이전트에 위임하면, 승인이 필요한 지점에서 작업이 실패하거나 자동 거부되는 경로로 흐를 수 있습니다. 이런 작업은 오케스트레이터 레벨에서 수행하거나, 서브에이전트에게는 진단·초안 생성만 시키고 실제 실행은 부모가 수행하는 분업 구조가 안전합니다.
형제 에이전트 간 실시간 통신 불가: 실행 중인 서브에이전트 A의 중간 결과를 B가 즉시 참조하는 채널은 없습니다. 필요하다면 A의 최종 결과를 받고, 그 결과를 프롬프트에 담아 B를 새로 시작하는 순차 구조로 변환해야 합니다.
보수적 기본 동작: 오케스트레이터는 명시적으로 지시하지 않으면 병렬 발주를 소극적으로 수행하는 경향이 있습니다. 팬-아웃을 유도하려면 "다음 N개 작업을 병렬로 위임하라"처럼 프롬프트에서 명시하는 편이 확실합니다.
모니터링과 디버깅
멀티 에이전트가 실행되면 "어떤 에이전트가 어떤 결정을 왜 내렸는지"를 사후 추적할 수단이 필수입니다. Anthropic 콘솔의 실행 이력에서 각 서브에이전트의 호출 시퀀스와 툴 사용 내역을 확인할 수 있으므로, 자체 로그와 함께 이를 활용하시길 권합니다. 애플리케이션 코드에서는 최소한 다음 정보를 로컬 로그에 남겨두면 재현이 훨씬 수월합니다.
- 각 서브에이전트에 넘긴 최종 프롬프트 원문
- 반환된 원본 응답과 JSON 파싱 성공 여부
- 시작·종료 타임스탬프와 사용 토큰 수
언제 병렬이 오히려 불리한가
병렬화가 항상 이득은 아닙니다. 아래 흐름을 따라 판단하면 잘못된 팬-아웃을 사전에 걸러낼 수 있습니다.
의존성이 있거나, 자원 경쟁을 격리할 수 없거나, 회수 결과가 부모 컨텍스트를 밀어낼 수준이면 병렬 팬-아웃은 오히려 손해입니다. 반대로 세 조건이 모두 통과하면 팬-아웃의 이득이 뚜렷해집니다.
마무리
Claude Agent SDK와 Claude Code CLI 모두 아직 활발히 변경되는 영역이라, 특정 함수명이나 옵션에 코드를 강하게 결합하기보다 위 판단 흐름과 결과 회수 계약(JSON 스키마·격리 정책·로그 계약)을 먼저 문서화해두는 편이 유지보수에 도움이 됩니다. 퍼블릭 베타 단계인 기능을 프로덕션에 투입하실 계획이라면, 팬-아웃 규모를 작게 시작해 컨텍스트 누적과 실패 패턴을 관찰한 뒤 점진적으로 넓혀가는 접근이 안전합니다.