본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 15. 10:57

첫 Claude Code 서브 에이전트 파이프라인을 설계하며 저지른 5가지 실수

요약

Claude Code를 활용해 멀티 에이전트 파이프라인을 구축하며 겪은 5가지 설계 실수를 분석합니다. 단일 프롬프트 사용, 비구조화된 텍스트 반환 등의 문제를 해결하고 효율적인 서브 에이전트 운영 방안을 제시합니다.

핵심 포인트

  • 모든 에이전트에 동일한 거대 프롬프트 대신 특화된 '단일 렌즈' 프롬프트 적용 필요
  • 자유 형식 텍스트 대신 구조화된 데이터 반환을 통해 파싱 오류 방지
  • 동시성 제한 및 중복 제거 로직을 통한 실행 효율성 및 결과 품질 개선

요약 (TL;DR)

저는 주말 동안 Claude Code를 사용하여 첫 멀티 에이전트 파이프라인 (multi-agent pipeline)을 구축하는 데 시간을 보냈고, 제가 내린 거의 모든 설계 선택이 틀렸음을 깨달았습니다. 여기에는 5가지 실수 — 하나의 거대한 프롬프트 (monolithic prompt), 자유 형식 텍스트 반환 (free-text returns), 성급한 장벽 (eager barriers), 무시된 동시성 제한 (ignored concurrency caps), 그리고 중복 제거 부재 (no dedup) — 가 있으며, 이를 어떻게 해결했는지 설명합니다. 만약 처음으로 서브 에이전트 (sub-agents)를 팬아웃 (fan out) 하려 한다면, 배포하기 전에 이 글을 읽어보시기 바랍니다.

문제 상황

저는 중간 규모의 코드베이스 전체에서 "이 리포지토리의 버그를 찾아라"라는 작업을 수행하고 싶었습니다. 단순한 버전은 쉬웠습니다. 하나의 Claude Code 세션, 하나의 커다란 프롬프트, 그리고 트리 구조를 탐색하는 방식이었습니다. 하지만 이는 속도가 느렸고, 진행 도중 컨텍스트 (context)가 부족해졌으며, 출력 결과는 제가 수동으로 다시 파싱해야 하는 구조화되지 않은 산문 덩어리였습니다.

그래서 저는 이를 팬아웃 (fan-out) 방식으로 다시 작성했습니다. N개의 서브 에이전트를 생성하여 각각 특정 슬라이스 (slice)에 집중하게 하고, 그 결과들을 수집하여 검증, 중복 제거 (dedupe), 보고하는 방식입니다. Claude Code 서브 에이전트 상단에서 실행되는 전형적인 맵리듀스 (map-reduce) 방식입니다.

첫 번째 버전은 "작동"했습니다. 즉, 결과물은 나왔습니다. 하지만 실제 소요 시간 (wall-clock)은 거대한 단일 구조 (monolith) 방식만큼이나 나빴고, 발견된 내용의 절반은 중복되었으며, 서브 에이전트 결과 5개 중 약 1개는 아예 파싱할 수 없었습니다. 저는 일주일 동안 세 번이나 다시 만들었습니다. 다음은 제 코드와 다른 사람들의 코드에서 계속해서 발견되는 실수들입니다.

버전: Claude Code v2.x, Node.js 22.x. 이 패턴들은 일반적이지만, 제가 참조하는 API 표면 (API surface)은 2025년 말/2026년 초 기준입니다.

해결 방법

5가지 실수를 하나씩 살펴보겠습니다. 각 실수마다 제가 실제로 작성했던 "이전" 스케치와, 최종적으로 도달한 버전을 보여드리겠습니다.

실수 1 — 모든 서브 에이전트에 동일한 거대한 프롬프트 사용

저의 첫 번째 팬아웃 (fan-out) 방식은 모든 서브 에이전트에게 동일한 프롬프트를 주고 입력 슬라이스 (input slice)만 변경하는 방식이었습니다:

const findings = await Promise.all(
  slices.map(slice =>
    runAgent(`Find bugs in this code. Look for correctness issues,
...

이것은 깔끔해 보입니다. 하지만 이것이 제 출력 결과가 노이즈가 많았던 이유이기도 합니다. 모든 에이전트가 제너럴리스트 (generalist)가 되려고 시도했고, 모든 에이전트가 동일한 얕은 문제들 (사용되지 않는 변수, 누락된 null 체크 등)을 반복해서 찾아냈으며, 그 누구도 어떤 문제에 대해서도 깊이 있게 파고들지 못했습니다.

해결책은 각 에이전트에게 **단일 렌즈 (single lens)**를 제공하는 것이었습니다. 입력은 동일하게 하되, 프롬프트(prompt)를 다르게 구성했습니다:

const LENSES = [
  { name: 'correctness', prompt: 'Find logic bugs. Ignore style.' },
  { name: 'security',    prompt: 'Find injection, auth, secret-handling bugs.' },
...

에이전트의 수는 동일하지만, 신호(signal)의 질은 훨씬 좋아졌습니다. 사전에 완전히 명시할 수 없는 무언가를 찾고 있을 때는 중복(redundancy)보다 다양성(diversity)이 승리합니다.

실수 2 — 자유 형식 텍스트 반환 (Free-text returns)

저는 에이전트가 산문(prose) 형태로 결과를 반환하게 두었다가, 나중에 정규 표현식(regex)으로 결과물을 추출하려고 시도했습니다. 반환된 결과의 약 20%에는 제가 예상하지 못한 헤더가 포함되어 있거나, 파서(parser)를 망가뜨리는 번호 매기기 방식이 있거나, 혹은 다음 단계의 데이터를 오염시키는 "참고로...(By the way…)"와 같은 꼬리말이 붙어 있었습니다.

해결책: 도구 계층(tool layer)에서 스키마(schema)를 강제하는 것입니다. 현재 대부분의 에이전트 프레임워크는 에이전트가 구조화된 출력(structured-output) 도구를 호출하도록 강제하는 기능을 지원합니다. Claude Code의 워크플로 프리미티브(workflow primitives)에서는 다음과 같이 구현됩니다:

const FINDING_SCHEMA = {
  type: 'object',
  required: ['findings'],
...

에이전트는 스키마 불일치 시 내부적으로 재시도(retry)를 수행하므로, 제가 객체를 돌려받을 때쯤이면 이미 유효한 상태가 됩니다. 이 단 한 번의 변화로 다운스트림(downstream) 코드가 절반으로 줄어들었고, 파싱 에러로 인한 비용(parse-error tax)이 사라졌습니다.

실수 3 — 단계 간의 성급한 장벽 (Eager barriers between stages)

저의 초기 파이프라인은 다음과 같은 모습이었습니다:

const reviews  = await Promise.all(items.map(reviewAgent))  // 장벽 (BARRIER)
const verified = await Promise.all(reviews.map(verifyAgent)) // 장벽 (BARRIER)

깔끔해 보이지만, 이는 모든 리뷰가 완료될 때까지 검증(verify) 단계가 시작될 수 없음을 의미합니다. 만약 한 명의 느린 리뷰어가 중앙값보다 3배 더 오래 걸린다면, 검증기는 그 시간 내내 아무것도 하지 못한 채 유휴 상태(idle)로 있게 됩니다.

해결책은 파이프라이닝(pipelining)입니다. 각 항목이 모든 단계를 독립적으로 통과하도록 만드는 것입니다. 항목 A가 검증 단계에 있는 동안 항목 B는 여전히 리뷰 단계에 있을 수 있습니다.

async function pipeline(items, ...stages) {
  return Promise.all(items.map(async (item) => {
    let cur = item
...

실제 소요 시간(Wall-clock)이 "단계별 가장 느린 작업의 합"에서 "가장 느린 단일 항목 체인"으로 단축되었습니다. 12개의 항목을 실행했을 때, 저의 경우 약 90초에서 약 35초로 차이가 났습니다.

배리어 (Barrier)는 스테이지 N이 실제로 스테이지 N-1의 모든 결과물을 필요로 할 때만 (전체 세트에 대한 중복 제거 (dedup), 발견 사항이 0일 경우 조기 종료 (early-exit), 항목 간 비교 등) 올바르게 작동합니다. 그렇지 않다면: 파이프라인 (pipeline) 방식을 사용하세요.

실수 4 — 동시성 제한 (concurrency cap) 무시

저는 신이 나서 80개의 항목을 Promise.all에 밀어 넣었습니다. 러너 (runner)는 이를 기쁘게 받아들였지만, 한 번에 10개씩 실행하면서 나머지 70개는 조용히 대기열 (queue)에 쌓아두었습니다. 제 로그에는 "80개의 에이전트 시작됨"이라고 표시되었지만, 실제로 작업을 수행 중인 것은 10개뿐이었고, 왜 실제 소요 시간 (wall-clock)이 그렇게 나쁜지 이유조차 알 수 없었습니다.

상황에 따른 두 가지 해결책은 다음과 같습니다:

  1. 제한 수치를 파악하세요. 대부분의 에이전트 러너는 동시성 제한 (concurrency cap)을 가지고 있습니다 (종종 min(16, CPU - 2) 형태). 이를 초과하는 모든 것은 대기열에 쌓입니다. 실제 소요 시간을 계산하고 싶다면, 이 제한 수치를 유효한 배치 크기 (batch size)로 취급하세요.
  2. 팬아웃 (fan-out) 규모를 적절히 조절하세요. 이제 저는 팬아웃을 "가능한 한 넓게"가 아니라, 작업 예산에 맞춰 조절합니다:
const BATCH = Math.min(items.length, MAX_CONCURRENCY)
log(`${BATCH}개를 동시 실행 중; ${items.length - BATCH}개가 대기열에 있음. `)

대기열에 쌓인 개수를 로그로 남긴 것은 이번 달 제가 한 디버깅 작업 중 가장 유용했습니다. 보이지 않던 병목 현상 (bottleneck)을 숫자로 바꾸어 주었습니다.

실수 5 — 검증 전 중복 제거 (dedup) 누락

검증 (Verification)은 비용이 많이 드는 단계입니다. 각 검증기 (verifier)는 파일을 읽고, 도구 (tools)를 실행하며, Claude에게 해당 주장을 반박하도록 요청합니다. 따라서 저의 탐지기 (finders)가 세 가지 서로 다른 관점에서 "이 함수는 입력 유효성 검사가 누락되었습니다"라는 결과를 내놓았을 때, 저는 동일한 발견 사항에 대해 3배의 비용을 지불하고 있었습니다.

해결책은 매우 단순합니다. 팬아웃 단계 사이의 일반 코드에서 중복 제거 (dedup)를 수행하는 것입니다:

const seen = new Set()
const fresh = allFindings.filter(f => {
  const key = `${f.file}:${f.line}:${f.description.slice(0, 60)}`
...

중복 제거 자체를 에이전트로 만들고 싶은 유혹(예: "Claude에게 유사한 발견 사항을 병합하도록 요청하기")이 생길 수 있습니다. 하지만 그러지 마세요. Set과 안정적인 키 (stable key)를 사용하는 것이 더 빠르고, 결정론적 (deterministic)이며, 비용도 들지 않습니다. 비교 작업에 진정한 판단 (judgment)이 필요한 경우에만 에이전트를 사용하세요.

최종적으로 도달한 형태

다섯 가지 수정을 모두 거친 후, 파이프라인은 대략 다음과 같은 모습이 되었습니다:

flowchart LR
    A[Slices] --> B[Lens 1: correctness]
    A --> C[Lens 2: security]
...

다중 렌즈 (mistake 1), 스키마 강제 반환 (2), 장벽 없는 파이프라인 검증 (3), 배치 인식 동시성 (4), 비용이 많이 드는 단계 전 중복 제거 (5).

Lessons Learned (교훈)

  1. 다양성이 중복을 이깁니다. 동일한 문제에 대해 N개의 에이전트를 생성한다면, 그들에게 N개의 서로 다른 관점을 부여하세요. 동일한 프롬프트를 N번 복제하는 것은 비용 낭비입니다.
  2. 스키마는 관료주의가 아니라 파서 (Parser)입니다. 도구 레이어에서 구조화된 출력 (Structured Output)을 강제하는 것은 멀티 에이전트 시스템 (Multi-agent System)에서 수행할 수 있는 가장 레버리지가 높은 변화입니다. 산문(Prose)을 정규 표현식 (Regex)으로 처리하는 일을 멈추세요.
  3. 기본적으로는 파이프라인화하고, 반드시 필요할 때만 장벽 (Barrier)을 두세요. "모든 것을 기다린 다음 다음 단계를 시작하겠다"는 대부분의 코드는 아무 이유 없이 실행 시간 (Wall-clock time)을 잡아먹는 세금과 같습니다. 장벽은 N 단계가 N-1 단계의 모든 항목으로부터 교차 항목 컨텍스트 (Cross-item context)를 필요로 할 때만 올바른 선택입니다.
  4. 동시성 제한 (Concurrency cap)은 실재하며 조용히 찾아옵니다. 러너 (Runner)가 대기열 (Queue)에 쌓이고 있다면 이를 반드시 알아야 합니다. 대기 중인 개수를 로그로 남기세요. 팬아웃 (Fan-out)의 크기를 당신의 야망이 아니라 제한 수치에 맞춰 적절히 조절하세요.
  5. 에이전트가 아닌 코드로 중복을 제거하세요. 저렴하고 결정론적인 연산 (필터링, 그룹화, 중복 제거, 정렬)은 LLM 호출이 아니라 스크립트에서 처리해야 합니다. 에이전트는 판단이 필요한 결정적인 순간을 위해 아껴두세요.

What's Next (다음 단계)

현재 실행 중인 버전에는 여전히 지나치게 신뢰하는 검증 단계가 있습니다. 만약 탐색기 (Finder)가 확신을 가지고 틀린 답을 내놓는다면, 단일 검증기 (Verifier)는 이를 그대로 승인 (Rubber-stamp)해 버릴 수 있습니다. 저는 적대적 패널 (Adversarial panel)을 실험 중입니다. 발견된 항목당 세 명의 회의론자 (Skeptics)를 배치하여, 각자 반박하도록 프롬프트를 작성하고 과반수가 반박하면 해당 항목을 폐기하는 방식입니다. 초기 결과는 유망해 보이지만 비용이 선형적으로 증가하므로, 이를 글로 정리하기 전에 정밀도/재현율 (Precision/Recall)을 제대로 측정해 보고 싶습니다.

또한 파이프라인의 전체 실행 시간 중 각 단계에서 가장 느린 단일 에이전트가 차지하는 비중을 추적하고 있습니다. 만약 지속적으로 특정 이상치 (Outlier)가 발생한다면, 무작정 기다리기보다는 타임아웃 후 재시도 (Timeout-and-retry)를 하는 것이 올바른 방향일 것입니다.

Wrap-up / CTA

만약 Claude Code 서브 에이전트 (sub-agents)를 활용해 무언가를 구축하고 있다면, 스키마 수정 (schema fix)을 먼저 시도해 보세요. 하루 안에 그 가치를 충분히 증명할 것입니다. 파이프라이닝 (pipelining) 변경은 더 규모가 크지만 침습적 (invasive)입니다. 출력 결과가 신뢰할 수 있을 만큼 충분히 안정화된 후에 진행하세요.

이 글이 도움이 되었다면:

  • Dev.to에서 저를 팔로우해 주세요 — 제가 겪는 에이전트 설계 (agent-design) 관련 실전 경험담들을 계속해서 작성할 예정입니다.
  • 아직 Claude Code를 사용해 보지 않으셨다면, 서브 에이전트 (sub-agent)와 워크플로우 프리미티브 (workflow primitives)가 이 모든 것을 가능하게 만든 핵심 요소입니다.
  • 여러분이 겪은 멀티 에이전트 (multi-agent) 관련 실수들을 댓글로 알려주세요 —

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0