하네스 엔지니어링으로 Claude Code 재현하기: AI 에이전트의 숨겨진 아키텍처
프롬프트가 아니라 하네스의 차이
2026년 초 현재, Claude Code는 출시 6개월 만에 연간 매출 10억 달러를 돌파했습니다. 처음 이 수치를 봤을 때 든 생각은 "프롬프트를 얼마나 잘 짰길래"였는데, 막상 들여다보니 그게 아니더라고요. Anthropic이 잘한 건 모델에게 더 영리한 지시를 내린 게 아니라, 올바른 모델을 중심으로 적절한 **하네스(harness)**를 짜 올린 쪽이었습니다.
최근 Claude Code의 실행 추적을 역공학한 결과가 공개됐는데, 그 핵심은 세 가지였습니다. 스트리밍 에이전트 루프, 권한 기반 도구 디스패치 시스템, 그리고 임의로 긴 세션 동안 모델이 집중 상태를 유지하게 만드는 컨텍스트 관리 레이어. 흥미로운 건 이 하네스가 완전히 재현 가능하다는 점입니다. 모델은 못 베껴도 모델을 둘러싼 구조는 베낄 수 있다는 뜻이죠.
나는 코드에서 시작해 아키텍처, 운영, 컨설팅까지 자리를 옮겨다니면서 비슷한 장면을 여러 번 봤습니다. 같은 엔진을 써도 그 엔진을 어떤 인프라 위에 올리느냐에 따라 결과물이 완전히 갈립니다. 이 글은 그 관점에서 Claude Code를 하나의 시스템으로 분해해 보려는 시도입니다.

하네스 엔지니어링의 4가지 핵심 원칙
하네스 엔지니어링은 AI 모델 자체가 아니라 모델을 둘러싼 환경을 다루는 영역입니다. 모델은 추론하고 결정합니다. 하네스는 그 결정을 실행하고, 제약하고, 바깥세계와 연결합니다. 역할이 분리되어 있다는 점이 중요합니다.
핵심 원칙은 네 가지로 정리됩니다.
- 모델은 의사결정의 유일한 원천 - 하네스는 모델 출력을 보고 멋대로 분기하지 않습니다. 모델이 요청한 것만 실행합니다.
- 도구는 모델과 세계 사이의 유일한 인터페이스 - 모든 작업은 타입이 지정되고 스키마 검증된 도구 호출을 거칩니다.
- 컨텍스트는 관리되는 자원 - 모델이 매 턴 보는 것은 그냥 쌓이는 게 아니라 의도적으로 선별되고 압축되어 주입됩니다.
- 권한은 선언적 - 허용, 차단, 승인 필요. 이 구분이 코드가 아니라 구성에 정의됩니다.
이 네 가지를 따로 보면 당연한 말처럼 들립니다. 그런데 운영을 해본 사람은 압니다. 이 분리가 무너지는 순간이 사고가 나는 순간이라는 걸요. 모델 출력을 보고 하네스가 "이건 이렇게 처리하자"며 자체 판단을 끼워 넣기 시작하면, 디버깅이 불가능해집니다. 누가 결정한 건지 추적이 안 되니까요.
1단계: 핵심 에이전트 루프
모든 것의 기반은 단순한 perception-action-observation 사이클입니다. 에이전트는 작업을 받고, 도구를 써서 해결을 시도하고, 결과를 관찰합니다. 그리고 계속할지 멈출지를 모델이 결정합니다.
def agent_loop(messages: List[Dict], dispatch: Dict):
while True:
response = client.messages.create(
model=MODEL,
system=DEFAULT_SYSTEM,
messages=messages,
tools=BASIC_TOOLS,
max_tokens=8000
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
break
results = dispatch_tools(response.content, dispatch)
messages.append({"role": "user", "content": results})
이 루프는 한 줄 수정이든 전체 코드베이스 리팩터링이든 똑같이 돕니다. 작업별 지능이 전부 모델 안에 있고, 루프는 그저 흐름만 관장하기 때문입니다. 루프가 똑똑해질 필요가 없다는 게 이 설계의 핵심입니다.
도구 디스패치 맵 패턴
Claude Code의 우아함은 도구가 많아서가 아닙니다. 새 도구를 추가하는 데 핵심 루프를 단 한 줄도 건드릴 필요가 없다는 점에 있습니다. 디스패치 맵이 그걸 가능하게 합니다.
DISPATCH = {
"bash": lambda inp: run_bash(inp["command"]),
"read": lambda inp: run_read(inp["path"]),
"write": lambda inp: run_write(inp["path"], inp["content"]),
"grep": lambda inp: run_grep(inp["pattern"])
}
루프는 어떤 도구가 존재하는지 전혀 모릅니다. 오직 dispatch[tool_name](input)을 호출하는 법만 압니다. 이건 플러그인 아키텍처의 고전적인 모습이기도 합니다. 코어와 확장을 분리해두면, 확장이 늘어나도 코어는 안정적입니다. 마이크로서비스에서 게이트웨이가 라우팅만 책임지고 비즈니스 로직을 모르는 것과 같은 발상이죠.
2단계: 지식 및 컨텍스트 관리
Claude Code의 92% 프롬프트 접두사 재사용률은 우연이 아닙니다. 시스템이 필요할 때만 도메인 지식을 로드하도록 설계됐기 때문입니다. 처음부터 다 들고 있는 게 아니라, 필요한 순간에 가져오는 구조입니다.
온디맨드 스킬 로딩
시스템 프롬프트에는 사용 가능한 스킬의 한 줄 설명만 들어갑니다. 모델이 "아, 지금 이 전문 지식이 필요하다"고 판단하면 load_skill()을 호출하고, 그제야 전체 명령이 정확한 순간에 대화에 직접 삽입됩니다.
code-review:
설명: 코드를 검토하거나 파일을 버그에 대해 감사할 때 사용
pdf:
설명: PDF 문서를 처리하고 추출할 때 사용
agent-builder:
설명: 새로운 에이전트나 하네스 컴포넌트를 설계할 때 사용
스킬을 백 개 설치해도 시스템 프롬프트는 백 줄이 됩니다. 백 페이지가 되지 않습니다. 이 차이가 토큰 비용과 모델 집중도를 동시에 지켜줍니다. 컨텍스트를 미리 채워두는 건, 캐시에 안 쓸 데이터까지 잔뜩 올려두는 것과 다르지 않습니다.
3계층 컨텍스트 압축
모든 장기 세션은 같은 벽에 부딪힙니다. 컨텍스트 창이 도구 출력과 중간 결과로 가득 차는 거죠. Claude Code는 약 92% 컨텍스트 창 사용량에서 자동으로 압축을 트리거합니다.
- 최근 메시지 - 능동적인 추론 컨텍스트라 그대로 유지
- 이전 메시지 - 전용 압축 API 호출로 단일 요약 블록으로 압축
- 요약 -
.agent_memory.md에 기록해 다음 세션에서 다시 로드
이건 로그 관리와 닮았습니다. 최근 로그는 원본 그대로 두고, 오래된 로그는 집계해서 줄이고, 그보다 더 오래된 건 아카이브로 내리는 것. 무한히 쌓이는 데이터를 다루는 시스템이라면 어디서든 보게 되는 패턴입니다.
3단계: 다중 에이전트 조정
단일 에이전트의 한계를 넘어서는 순간, 병렬 실행과 전문화가 핵심이 됩니다.
지속적인 팀원과 FSM 프로토콜
Claude Code는 일시적인 서브에이전트 외에도, 여러 작업에 걸쳐 살아남는 전문가들을 운영합니다. 각 팀원은 자기만의 전문화와 JSONL 파일 받은편지함을 갖고 백그라운드 스레드에서 계속 돕니다.
통신은 유한 상태 머신(FSM)으로 조정됩니다.
- IDLE: 새 작업을 받을 수 있음
- REQUESTING: 다른 에이전트에게 요청 중
- WAITING: 응답 대기 중
- RESPONDING: 작업 처리 중
핵심 규칙은 하나입니다. 대기 상태에서는 새로운 요청을 할 수 없습니다. 이 단 하나의 규칙이 교착 상태를 통째로 제거합니다. 분산 시스템에서 데드락은 대부분 "내가 너를 기다리는데, 너도 나를 기다리는" 순환에서 생깁니다. 대기 중인 노드가 새 요청을 못 걸게 막으면, 그 순환 자체가 만들어지지 않습니다. 우아한 방식이죠.
Git Worktree 작업 격리
병렬 에이전트가 같은 파일에 쓰면 충돌이 납니다. Git worktree가 해답입니다. 각 에이전트는 저장소에 대한 자기 디렉터리, 자기 브랜치, 완전한 작업 트리를 갖습니다.
def create_worktree(task_id: str) -> tuple[str, str]:
branch = f"task/{task_id}"
path = f".worktree-{task_id[:8]}"
subprocess.run(["git", "worktree", "add", "-b", branch, path])
return path, branch
두 에이전트가 "core.py"를 동시에 수정해도, 실제로는 서로 다른 디렉터리의 서로 다른 파일에 쓰고 있습니다. 파일 자체가 별개라 쓰기 충돌이 원천적으로 불가능합니다. 락으로 충돌을 막는 게 아니라, 충돌할 일이 없는 구조를 만든 겁니다. 동시성 문제를 푸는 가장 좋은 방법은 보통 "공유를 없애는" 쪽이라는 걸 다시 확인하게 됩니다.
4단계: 프로덕션 강화
작동하는 에이전트와 배포 가능한 에이전트 사이에는 격차가 있습니다. 이 단계가 그 격차를 메웁니다.
실시간 토큰 스트리밍
Claude Code에서 스트리밍은 기능이 아니라 기본값입니다. 각 토큰이 생성되는 대로 터미널로 흘러나갑니다. 수십 개의 도구 호출에 걸친 긴 추론 체인에서, 차단(blocking) 방식 에이전트는 몇 분 동안 침묵합니다. 스트리밍 에이전트는 그 시간을 실시간으로 생각을 보여주는 데 씁니다.
사소해 보이지만 운영 관점에선 큰 차이입니다. 사용자는 "멈춘 것"과 "오래 걸리는 것"을 구분하지 못합니다. 화면이 5초만 정지해도 사람들은 새로고침을 누릅니다. 진행 상황이 계속 흐르면 같은 대기 시간도 견딜 만해집니다.
YAML 규칙 기반 권한 거버넌스
Claude Code의 권한 시스템은 3계층 모델을 씁니다.
always_deny:
- pattern: "rm -rf /"
reason: "무조건 재귀 루트 삭제"
always_allow:
- pattern: "^ls( |$|-)"
reason: "파일을 나열하는 것은 항상 안전"
ask_user:
- pattern: "^rm "
reason: "파일 삭제는 확인이 필요"
보안 정책이 코드가 아니라 구성에 있습니다. 이게 왜 중요할까요? 승인이 필요한 변경을 배포 없이, 구성 파일 편집만으로 끝낼 수 있기 때문입니다. 금융권 IT에서 권한 변경 한 줄을 반영하려고 전체 배포 절차를 다시 타야 했던 경험을 떠올리면, 이 선언적 분리의 가치가 더 크게 다가옵니다. 정책과 코드가 붙어 있으면 정책을 바꿀 때마다 코드 위험을 짊어져야 하니까요.

5단계: 고성능 비동기 런타임
병렬 도구 실행
실행 추적 분석에서 드러난 Claude Code의 가장 중요한 성능 특성 중 하나는, 굳이 그럴 필요가 없을 때 도구 호출을 순차로 돌리지 않는다는 점입니다.
한 턴에 grep 세 번, 읽기 두 번이 담긴 응답이 오면, 다섯 개가 동시에 실행됩니다. 턴은 가장 느린 단일 호출 시간에 끝납니다. 다섯 개를 더한 시간이 아닙니다. 이 차이가 긴 세션에서 누적되면 체감 속도가 완전히 달라집니다.
async def agent_loop(messages):
tool_blocks = [b for b in response.content if b.type == "tool_use"]
if len(tool_blocks) > 1:
print(f"Running {len(tool_blocks)} tools in parallel...")
# 모든 도구 호출을 한 번에
pairs = await asyncio.gather(*[_dispatch_one(b) for b in tool_blocks])
프롬프트 캐싱 최적화
시스템 프롬프트와 도구 정의는 모든 에이전트 세션에서 가장 안정적인 콘텐츠입니다. 거의 바뀌지 않죠. 이를 캐시 가능하도록 표시하면, 첫 호출 이후의 모든 호출이 해당 토큰을 약 10% 비용으로 받습니다.
SYSTEM_BLOCKS = [
{
"type": "text",
"text": "You are a coding agent...",
"cache_control": {"type": "ephemeral"}
}
]
수백 번 호출이 오가는 세션 전체로 보면, 이건 무시할 수 없는 비용 절감입니다. 안 바뀌는 걸 매번 다시 계산하지 않는다 - 캐싱의 본질은 어디서나 같습니다.
6단계: 엔터프라이즈 확장
Redis Pub/Sub 메일박스
교육용으로 쓰던 JSONL 메일박스를, 즉시 전달과 크로스 머신을 지원하는 Redis pub/sub 채널로 교체합니다. 에이전트가 채널에 게시하면 모든 구독자가 밀리초 안에 받습니다. 폴링 루프도, 파일 잠금도, 파일 시스템 의존성도 없습니다. 단일 머신 가정을 벗어나는 순간, 파일 기반 통신은 곧장 병목이 됩니다. 이 교체는 그 한계를 정면으로 풀어줍니다.
MCP 런타임 통합
Claude Code는 MCP를 기본 지원합니다. 호환되는 서버의 도구가 에이전트의 도구 레지스트리에서 일급 시민이 됩니다. 파일 시스템 서버는 파일 도구를, git 서버는 git 운영 도구를, 데이터베이스 서버는 쿼리 도구를 더합니다.
모델은 모든 도구를 내장 도구와 똑같이 호출합니다. 그 도구가 로컬 Python 함수인지, 원격 서버 프로세스인지 모릅니다. 추상화가 제대로 됐다는 신호죠. 호출하는 쪽이 구현 위치를 신경 쓰지 않아도 되면, 확장은 자유로워집니다.
실제 Claude Code와의 차이점
이 재현 구현과 실제 Claude Code 사이에는 몇 가지 차이가 있습니다.
| 구분 | 재현 구현 | 실제 Claude Code |
|---|---|---|
| 도구 수 | 23개 컴포넌트 | 18개 등록 도구 |
| 캐시 히트율 | 83% (테스트) | 92% (프로덕션) |
| 압축 트리거 | 40K 토큰 | 동적 임계값 |
| 세션 지속성 | JSON 파일 | 분산 스토어 |
수치는 다르지만, 핵심 아키텍처 원칙과 하네스 엔지니어링 접근법은 동일합니다. 프로덕션과 재현 사이의 격차는 대부분 "규모를 견디는 디테일"에서 나옵니다. 구조 자체가 다른 게 아니라요.

마치며
Claude Code의 성공을 따라가다 보면 한 가지가 분명해집니다. 더 똑똑한 모델이나 더 나은 프롬프트가 출발점이 아니었다는 것. 출발점은 올바른 하네스였습니다.
나는 여러 자리에서 같은 교훈을 반복해서 만났습니다. 엔진을 직접 깎으려는 시도보다, 그 엔진이 일관되고 예측 가능하게 돌아갈 구조를 짜는 쪽이 결과적으로 더 멀리 갑니다. 모델은 빠르게 발전하고, 곧 교체될지도 모릅니다. 하지만 그 모델을 감싸는 루프, 도구 인터페이스, 컨텍스트 관리, 권한 거버넌스는 한 번 잘 설계해두면 오래 살아남습니다.
결국 진짜 자산은 모델이 아니라 하네스 쪽에 쌓이는 게 아닐까. Claude Code를 뜯어보며 든 생각은 거기에 가깝습니다.