사내 Claude Code 세미나 1주차 회고: 한 번의 API 요청 안에는 무엇이 들어있나
사내 세미나 첫 주차를 마쳤다. 발표를 준비하면서 가장 크게 흔들린 것은 다름 아닌 나 자신의 멘탈 모델이었다. "Claude Code가 알아서 도구를 안다"라고 막연히 믿어온 것이, 실제로는 매 요청마다 새로 조립되는 한 덩어리 JSON이었다는 사실을 다시 마주했다.
기술연구소 챕터원들 앞에서 Claude Code 시리즈 첫 발표를 했다. 4~6주 분량으로 잡아둔 세미나의 출발이라, 본격적인 활용 기법보다 "이 도구가 도대체 무엇으로 만들어진 도구인가" 를 먼저 깔고 가자고 결정했다. 발표가 끝나고 받은 질문의 대부분이 "그럼 우리 CLAUDE.md는 어디에서 합쳐지는 거예요?", "MCP 도구가 갑자기 사라지는 건 왜죠?" 같은 것이었는데, 결국 모두 같은 한 가지 그림을 그리지 못해서 생기는 혼란이었다. 이 글은 그날 화이트보드에 그렸던 그림을 그대로 옮겨 적은 회고에 가깝다.
1. 출발점: Claude Code는 결국 한 번의 API 호출이다
먼저 모두에게 가장 먼저 깔아준 명제는 이것이었다.
"사용자 메시지 한 줄이 들어오면, Claude Code는 Anthropic API에 단일 요청을 만든다. 그 안에 우리가 본 모든 마법이 들어있다."
발표 자료의 첫 장은 이 단일 요청의 구조였다. 백엔드 개발자에겐 친숙한 그림이다: 잘 만들어진 RPC 한 번이라고 생각하면 된다.
요청의 페이로드는 정확히 네 가지 키를 가진 JSON이다.
- ① model:
claude-opus-4-7같은 모델 식별자. 어떤 모델로 추론할지. - ② system: 시스템 프롬프트. 텍스트 한 덩어리지만, 실은 여러 소스에서 조립된 결과.
- ③ tools: 사용 가능한 도구 스키마 배열. 각 도구의 이름·설명·JSON Schema 전체를 포함.
- ④ messages: 지금까지의 대화 기록.
user / assistant / tool_result가 순서대로 누적.
이 네 가지가 매 요청마다 새로 조립되어 한 번에 날아간다. 발표장의 백엔드 개발자들에게 가장 와닿았던 비유는 "Claude Code의 '세션'은 stateful한 척하는 stateless API"라는 표현이었다. 우리가 보는 연속성은 모두 messages 배열의 누적으로 흉내 낸 것이다.
2. system 프롬프트의 해부: 우리 CLAUDE.md는 어디에서 합쳐지는가
가장 많은 질문이 몰린 부분이다. system 프롬프트는 텍스트지만, 그 안에 들어가는 출처는 명확하게 정해져 있다.
[Claude Code 기본 지침] ← 하드코딩된 행동 규칙·보안 규칙
[CLAUDE.md] ← 프로젝트/사용자/조직 설정
[MEMORY.md (200줄)] ← 자동 메모리 인덱스
[환경 컨텍스트] ← OS · Shell · Git 브랜치 · 오늘 날짜
<available-deferred-tools>
AskUserQuestion, CronCreate, TaskCreate, mcp__chrome__*, ...
</available-deferred-tools>
<system-reminder>
Available skills: simplify, security-review, /init …
</system-reminder>
[Plugin Agent description]이 모든 것이 세션 시작 시 한 번 조립되어, 그 뒤로는 매 API 요청마다 그대로 들어간다. 즉, 우리가 CLAUDE.md에 적는 모든 규칙은 사용자 메시지가 아니라 system 프롬프트의 일부가 되는 것이다. 이게 왜 중요하냐면, system 프롬프트는 사용자 메시지보다 우선순위가 높다. 단, 그렇다고 시스템 프롬프트 = 강제는 아니다. 모호하면 무시된다. 이 단서는 다음 주차(Memory) 발표에서 또 한 번 강조했다.
자주 헷갈리는 두 개의 태그
발표 중 가장 많이 받은 후속 질문이 <available-deferred-tools>와 <system-reminder>의 차이였다.
<available-deferred-tools>: 도구 이름만 나열. 스키마는 없다.ToolSearch를 호출해야 비로소 스키마가 로드되어 호출 가능해진다.<system-reminder>: 사용 가능한 Skills 목록과 각 스킬의 트리거 조건이 들어간다. Skill은 도구가 아니라 "현재 대화에 주입되는 프롬프트 조각"이다.
두 개 다 시스템 프롬프트 안에 들어가지만, 하나는 "불러올 수 있는 도구의 명함" 이고 다른 하나는 "의식적으로 활성화시킬 수 있는 행동 모드 목록" 이다. 본질이 다르다.
3. tools 배열: 모델이 도구를 "아는" 방식
여기가 가장 강조하고 싶었던 지점이다. 발표 슬라이드에 통째로 한 줄을 띄워놓고 읽었다.
"모델은 자기가 쓸 수 있는 도구를 미리 알지 않는다. 매 요청마다 tools 배열로 새로 주입받고, 그 스키마를 보고 즉석에서 판단한다."
{
"tools": [
{
"name": "Read",
"description": "Reads a file from the local filesystem...",
"input_schema": {
"type": "object",
"properties": {
"file_path": { "type": "string" },
"offset": { "type": "integer" },
"limit": { "type": "integer" }
},
"required": ["file_path"]
}
},
{
"name": "Agent",
"description": "Launch a new agent...\nAvailable agent types:\n- Explore: ...\n- Plan: ..."
},
{ "name": "mcp__claude-in-chrome__navigate", "...": "..." }
]
}여기서 두 가지 인사이트를 끌어냈다.
- MCP 도구는 내장 도구와 차별 없이 같은 배열에 들어간다. 모델 입장에선
Read나mcp__db__query나 똑같다. 단지 이름 규칙이mcp__{server}__{tool}로 정해져 있을 뿐. - Agent 도구의 description 안에 "사용 가능한 subagent 목록"이 들어간다. 즉, Plugin Agent를 추가하면 새로운 tool이 늘어나는 게 아니라
Agent도구의 설명이 길어지는 것이다. 이걸 알면 "어떤 subagent를 쓸 수 있죠?"라는 질문에 더 이상 추측으로 답하지 않게 된다.
4. Deferred Tools: 컨텍스트 비용을 다루는 방식
세미나 후반부에서 모두가 "아…" 하고 고개를 끄덕인 지점이다. 모든 도구 스키마를 한 번에 보내면 컨텍스트 윈도우를 낭비한다. 그래서 Anthropic은 자주 안 쓰는 도구는 이름만 알려두고, 필요할 때 ToolSearch로 스키마를 가져오는 지연 로딩 패턴을 도입했다.
- 처음 API 요청: tools 배열에는 풀 스키마 도구만, system 프롬프트엔
<available-deferred-tools>에 이름만 나열 - Claude가 필요를 감지:
ToolSearch를 호출해서 도구 이름을 지정 - 응답으로 풀 스키마가 돌아옴: 이제부터 그 도구는 실제로 호출 가능
백엔드 시각으로 비유하면 lazy class loading과 똑같다. JVM이 클래스 패스만 알고 있다가, 처음 사용될 때 실제 바이트코드를 메모리에 올리는 그 동작이다. 컨텍스트 윈도우라는 한정된 자원을 절약하기 위한 너무도 익숙한 패턴이라, 발표 직후 모두가 이 부분을 가장 빠르게 이해했다.
5. Skills와 Plugin Agents: 같은 이름의 두 가지 다른 메커니즘
여기서 처음으로 시간을 길게 잡고 설명했다. 이름이 비슷한데 동작이 정반대다.
| Skills | Plugin Agents | |
|---|---|---|
| 위치 | system 프롬프트 <system-reminder> | Agent 도구의 description |
| 실행 방식 | Skill 도구 호출 → 현재 대화에 프롬프트가 주입됨 | Agent 도구 호출 → 독립 Claude 인스턴스 생성 |
| 컨텍스트 | 메인과 공유 | 완전히 분리 |
| 비유 | "매뉴얼을 펼쳐서 읽으며 일한다" | "외주 전문가에게 작업을 통째로 맡긴다" |
Skills는 모드 전환에 가깝고, Plugin Agents는 위임이다. 이 둘을 같은 단어("확장")로 묶어 설명하던 나의 옛 자료를 발표 직전에 모두 갈아치웠다. 한 번 헷갈리면 둘을 끝까지 헷갈리게 된다.
6. 모델 루프: 한 번의 사용자 요청이 끝날 때까지
한 번의 user 메시지에 대해 Claude는 다음 사이클을 stop_reason: end_turn이 나올 때까지 반복한다.
user → Claude 추론 (tool_use 결정)
→ 클라이언트가 로컬에서 도구 실행
→ tool_result를 messages에 추가
→ 다시 Claude 추론 (다음 tool_use? 아니면 텍스트 응답?)
→ ... 반복 ...
→ end_turn (최종 텍스트 응답)여기서 발표 중 가장 좋은 질문이 나왔다. "이 루프 안에서 도구를 병렬로 호출할 수 있나요?" 답은 그렇다. 같은 응답 안에서 여러 개의 tool_use 블록을 동시에 반환하면, 클라이언트가 병렬로 실행한다. 시니어 백엔드 개발자라면 익숙한 그림: 독립적인 I/O는 묶어서 한 라운드트립으로 끝낸다.
7. Sub-Agent: 메인과 분리된 또 하나의 세션
Agent 도구는 결국 또 하나의 독립 Claude 세션을 띄우는 일이다. 메인 Claude의 컨텍스트를 오염시키지 않으면서 무거운 탐색·리뷰·디버깅을 위임할 수 있다. 발표에서는 한 줄 비유로 닫았다.
메인 Claude는 본인이 직접 만나는 동료, Sub-Agent는 외주를 맡기는 전문가다. 동료에게 시키면 그 사람 머리에 우리 대화가 다 쌓이지만, 전문가에게는 결과 요약만 받아온다.
이 패러다임은 5주차에 sub-agents를 본격적으로 다룰 때 다시 깊이 들어가기로 했다.
8. 발표에서 가장 좋았던 질문과 그 답
- "CLAUDE.md를 100줄 적으면 매 요청에 100줄이 다 들어가나요?"
들어간다. 그래서 200줄 이하를 권장하고, 자주 안 쓰는 규칙은
.claude/rules/로 빼서 paths 기반으로 로드되게 만드는 게 정석이다. - "MCP 서버가 죽으면 어떻게 되나요?"
세션 시작 시점에 연결이 안 되면 그 도구가 tools 배열에 들어오지 않는다. 즉, 모델이 도구의 존재 자체를 모르는 상태가 된다. 침묵하는 실패는 아니지만 눈에 잘 띄지도 않는다.
claude --debug로 확인 가능. - "왜 Deferred Tools에 MCP 도구가 많이 들어가요?" MCP 도구는 스키마가 무겁고 모두 자주 쓰지도 않기 때문이다. 컨텍스트 비용이 곧 비용이라는 것을, MCP 도구를 다섯 개 이상 붙이면 바로 체감하게 된다.
9. 시니어의 시선으로 정리하는 인사이트
① "Claude Code = 단일 API 호출"이라는 멘탈 모델이 모든 것의 출발이다
세션·메모리·도구·에이전트·스킬을 별개의 개체로 보면 영원히 길을 잃는다. 모든 것이 결국 매 요청마다 새로 조립되는 한 덩어리 JSON으로 수렴한다. 우리가 만지는 모든 설정은 이 JSON의 어딘가에 들어가는 것이다. 백엔드 개발자에게 이 비유는 너무도 친숙하다: 서버 사이드 렌더링의 한 페이지가 매 요청마다 다시 조립되는 그 그림과 정확히 같다.
② tools 배열은 "이번 라운드에 모델이 가진 손"이다
모델은 라운드마다 손에 쥔 도구로 다음 수를 결정한다. 그러니 MCP 도구를 욕심내서 많이 붙이면 손이 무거워진다. 무엇이 가능한지를 모델에게 알려주는 비용은 곧 컨텍스트 비용이다. 시니어 입장에서 이 결정은 "무엇을 줄이느냐" 의 결정이지, "무엇을 더 붙이느냐"의 결정이 아니다.
③ Deferred · Lazy 로딩은 시스템 설계의 보편적 원리다
Spring의 Lazy Bean, Hibernate의 Lazy Loading, Java의 Class Loader, Webpack의 Code Splitting: 모두 같은 원리다. 한정된 자원(메모리 · 컨텍스트 · 초기화 시간)을 다룰 땐 사용 시점까지 비용을 미루는 것이 정답인 경우가 많다. Claude Code의 Deferred Tools와 Skill 3단계 로딩은 이 원리를 LLM 컨텍스트 윈도우에 그대로 적용한 사례다.
④ Skills와 Plugin Agents의 차이는 "주입 vs 위임"이다
이 한 줄을 사내에 정착시킬 수 있다면, 앞으로 Skill·Agent 관련 모든 토론이 5분씩 빨라진다. 발표에서 가장 보람 있었던 한 줄이기도 했다.
마무리: 도구를 안다는 것은 도구의 인터페이스를 안다는 뜻이다
발표를 마치고 회의실을 나오면서 한 분이 이런 말을 했다. "백엔드 개발자가 ORM 안을 한 번도 안 들여다보고 ORM을 쓰는 것과, Claude Code를 매일 쓰면서 그 안의 API 모양을 모르는 것은 같은 문제더라고요." 정확히 내가 이 발표를 준비하면서 깨달은 그 지점이었다.
회사의 누군가가 Claude Code를 처음 만났을 때, 화면 너머에서 무엇이 오가고 있는지를 한 장의 그림으로 설명할 수 있는 사람이 한 명이라도 더 늘어났다면, 그것만으로 이번 주차의 목적은 충분히 달성된 것이다. 본인의 학습이 깊어지는 가장 빠른 길은 결국 다른 사람에게 그것을 설명하는 자리에 서는 일이라는, 너무도 진부하지만 매번 다시 확인하게 되는 명제. 다음 주는 Memory 편이다. 이번 주에 깔아둔 "system 프롬프트의 해부"를 그대로 들고 가면 절반은 이미 끝난 셈이다.