무엇을 직접 짜고 무엇을 맡길까 — 손으로 짠 골격을 공식과 나란히
2026-04-10
1편부터 4편까지, 에이전트의 뼈대를 프레임워크 없이 손으로 짰습니다 — while 루프(1편), 도구와 예산·병렬(2편), 컨텍스트 요약(3편), 메모리(4편). 이제 방향을 바꿔 봅니다. 원리를 아는 지금, Anthropic이 공식으로 내놓은 것들은 이 중 무엇을 대신 해 주는지, 그리고 실무에서 무엇을 직접 짜고 무엇을 맡기는 게 좋은지를요.
여기서 오해 하나를 먼저 풉니다. "직접 짜기"와 "공식 도구 쓰기"는 양자택일이 아닙니다. 진짜 질문은 *"어디에 선을 긋느냐"*예요.
규칙 — 원리를 안다면, 무엇을 직접 소유하고 무엇을 맡길지 경계를 그어라. 소유할 것은 루프와 정책이고, 맡겨도 되는 것은 트랜스포트다.
이번 편도 코드로 증명합니다 — 그 경계를 인터페이스 하나로 실제로 그어 봅니다.
💻 코드는 github.com/kahnco/agent-from-scratch에 이어집니다. HTTP 없는 순수 fake로 그 이음매가 진짜 갈리는지 검증합니다.
지도 — 우리가 손으로 짠 것의 공식 대응물
우리가 4편에 걸쳐 짠 부품들은, 사실 공식 세계에 각자 대응물이 있습니다. 손으로 짜 봤기에 이제 그게 무엇을 감추는지 보입니다.
| 우리가 짠 것 | 무엇이었나 | 공식 세계의 대응물 |
|---|---|---|
while 루프(1편) |
tool_use면 실행하고 다시 부르기 | SDK Tool Runner가 이 루프를 자동화 |
병렬 도구 실행·MaxSteps(2편) |
goroutine + 스텝 상한 | Tool Runner의 반복 관리 / Managed Agents가 대신 |
| 컨텍스트 요약(3편) | 넘치면 앞부분 접기 | Managed Agents·Agent SDK의 컨텍스트 관리 |
| 메모리(4편) | 대화 밖 key-value | Claude의 memory 도구 / Managed Agents 메모리 |
| 도구 실행 자체 | Run 함수를 우리가 실행 |
Managed Agents는 샌드박스에서 대신 실행 |
핵심은 이겁니다 — 우리가 손으로 짠 것들은 하나같이 "누군가 대신 해 줄 수 있는" 것들이었어요. 그런데도 직접 짠 이유는, 그래야 무엇이 감춰져 있는지 알기 때문입니다. Tool Runner가 무한 루프에 안 빠지는 건 우연이 아니라 내부에 우리가 2편에서 짠 것 같은 예산이 있어서고, Managed Agents가 긴 대화를 버티는 건 3편 같은 접기가 안에 있어서죠.
규칙 회수 — 손으로 짠 부품마다 공식 대응물이 있다. 직접 짜 봤기에, 공식 도구가 무엇을 대신하는지 이제 안다.
경계를 인터페이스로 긋다
그럼 실제로 "맡기려면" 뭘 해야 할까요? 지금 우리 에이전트는 우리가 1편에서 짠 raw-HTTP Client에 직접 묶여 있습니다. 이걸 공식 SDK로 바꾸려면 루프를 뜯어고쳐야 할까요? 아뇨 — 경계를 인터페이스로 그으면 됩니다.
에이전트가 모델을 부르는 창구는 단 하나, Create 하나뿐이었습니다. 그러니 그것만 인터페이스로 빼면 돼요.
// 에이전트가 모델을 부르는 유일한 창구다. 이 뒤에 무엇이 있든
// — 우리 raw HTTP Client, 공식 SDK 어댑터, 다른 제공자 — 루프는 모른다.
type LLM interface {
Create(ctx context.Context, system string, messages []Message, tools []Tool) (*Response, error)
}
그리고 Agent가 구체 타입 대신 이 인터페이스에 의존하게 바꿉니다. 딱 한 줄이에요.
type Agent struct {
Client LLM // *Client 가 아니라 인터페이스 — 트랜스포트를 갈아 끼울 수 있다
// ...
}
이 한 줄이 경계입니다. 인터페이스 위쪽(루프·도구 정책·예산·메모리)은 우리가 소유하고, 아래쪽(HTTP·재시도·직렬화)은 누구에게 맡겨도 됩니다. 우리 raw-HTTP Client는 이미 Create를 가지니 그대로 만족하고(var _ LLM = (*Client)(nil)로 컴파일 타임에 확인합니다), 내일 공식 SDK로 바꾸고 싶으면 SDK를 감싼 얇은 어댑터 하나만 LLM으로 만들어 끼우면 루프는 한 줄도 안 바뀝니다.
이게 말뿐이 아니라는 걸, HTTP 서버조차 없는 순수 fake로 증명합니다.
type fakeLLM struct{ replies []*Response; i int }
func (f *fakeLLM) Create(_ context.Context, _ string, _ []Message, _ []Tool) (*Response, error) {
r := f.replies[f.i]; f.i++; return r, nil // 그냥 미리 준비한 응답을 순서대로
}
func TestAgentRunsOnAnyLLMImplementation(t *testing.T) {
a := &Agent{Client: fake, Tools: /* calculator */}
out, _ := a.Run(context.Background(), "2+3?")
// 루프는 뒤에 HTTP 가 있는지 fake 가 있는지 전혀 모른다 → "5 입니다."
}
루프는 뒤에 무엇이 있는지 모릅니다. 이 무지가 곧 자유예요 — 트랜스포트를 언제든 갈아 끼울 자유죠. Flutter 실전 4편에서 저장소를 sqflite로 바꾸는데 도메인은 한 줄도 안 고쳤던 것과 정확히 같은 결입니다.
규칙 회수 — 소유할 것과 맡길 것 사이에 인터페이스를 둬라. 그러면 루프(우리 것)를 지키면서 트랜스포트(맡길 것)를 자유롭게 바꾼다.
그래서, 언제 직접·언제 맡기나
Anthropic이 에이전트를 위해 내놓은 길은 크게 넷입니다. 우리가 지금 서 있는 자리부터 짚으면 이렇게 정리됩니다.
- 직접 while 루프(우리가 한 것). 루프·컨텍스트·정책을 전부 우리가 소유합니다. 의존성이 없고 완전히 투명하죠. 원리를 배울 때, 제어를 한 톨도 양보하기 싫을 때, 특수한 제어 흐름이 필요할 때 값이 있습니다. 대신 예산·요약·메모리를 다 직접 관리해야 해요(우리가 4편에 걸쳐 한 게 그거고요).
- SDK Tool Runner. SDK가 도구 루프를 대신 돌려 줍니다(Go라면
toolrunner패키지). 우리가 짠while을 손으로 안 짜도 되고, 승인 게이트·재시도·스트리밍 같은 것도 딸려 오죠. 하지만 여전히 내 서버에서, 내가 정의한 도구로 돕니다. 직접 짠 루프의 값을 대부분 누리면서 보일러플레이트만 덜고 싶을 때 자연스러운 다음 칸입니다. - Managed Agents. 루프뿐 아니라 도구가 실행되는 샌드박스까지 Anthropic이 호스팅합니다. 파일·bash·코드 실행이 서버 컨테이너에서 돌고, 세션·버전·스케줄까지 관리돼요. 오래 도는 상태 있는 에이전트, 워크스페이스가 필요한 작업, 스케줄로 자율 실행이 필요할 때 강력합니다. 대신 실행 환경을 Anthropic에 맡기는 것이고요.
- Claude Agent SDK. 이건 결이 다른 별도 제품입니다 — Claude Code를 라이브러리로 만든 것으로, 파일 읽기·쓰기·bash·검색 같은 내장 도구와 완성된 하네스가 딸려 옵니다. 코딩/파일시스템 에이전트를 내 인프라 위에서 빠르게 세울 때죠. (지금 이 시리즈를 쓰는 Claude Code가 그 위에 있습니다.)
한 문장으로 줄이면 — 배우고 완전히 제어하려면 직접, 보일러플레이트만 덜려면 Tool Runner, 실행 환경까지 맡기려면 Managed Agents, 코딩 에이전트를 빨리 세우려면 Agent SDK입니다.
규칙 회수 — 네 길은 "제어를 얼마나 쥐고 무엇을 맡기냐"의 스펙트럼이다. 원리를 알면 내 상황에 맞는 칸을 고를 수 있다.
정직하게 — 직접 짠 것의 값과 한계
직접 짜 온 이 시리즈의 결론이 "그러니 직접 짜라"는 아닙니다. 정직하게 봅니다.
- 프로덕션이라면 대개 공식을 얹는 게 낫다. 우리가 짠 건 원리를 드러내는 최소 골격이지, 재시도·레이트리밋·스트리밍·관측·에러 분류 같은 프로덕션 필수품은 없습니다. 그걸 다 직접 짜는 건 바퀴의 재발명이에요. 직접 짠 이해를 밑천 삼아, 실무에선 Tool Runner나 Managed Agents를 얹는 게 보통 옳습니다.
- 직접 짠 것의 진짜 값은 투명성과 제어다. 의존성이 0이고, 모든 줄이 눈에 보이고, 어떤 제어 흐름이든 넣을 수 있죠. 프레임워크가 감춘 걸 디버깅할 때, 특수 요구가 있을 때, 그리고 무엇보다 배울 때 이게 값을 합니다. 이 시리즈가 노린 게 그거고요.
- 경계(인터페이스)는 어느 쪽을 고르든 남는 자산이다. 오늘 이 편에서 그은
LLM경계는 직접 짜든 맡기든 유효합니다 — 트랜스포트를 나중에 바꾸거나, 여러 제공자를 나란히 두거나, 테스트에서 fake를 물리는 게 다 이 한 줄 덕이에요. 아키텍처의 이음매는 프레임워크 선택보다 오래갑니다. - "직접 vs 맡김"은 한 번 정하고 끝이 아니다. 처음엔 직접 짜 배우고, 프로토타입은 Tool Runner로 빠르게, 스케일에선 Managed Agents로 — 이렇게 옮겨 다녀도 됩니다. 경계만 잘 그어 두면 갈아타는 비용이 작으니까요.
이번 편으로 시리즈의 한 매듭을 지었습니다. 손으로 짠 골격(1~4편) 위에, 그것이 공식 세계 어디에 서 있는지(5편)까지 그렸어요.
정리 — 선을 긋는 문제
- 오해 풀기: "직접 짜기 vs 공식"은 양자택일이 아니라, 어디에 경계를 긋느냐의 문제다.
- 지도: 우리가 짠 부품(루프·예산·요약·메모리)마다 공식 대응물(Tool Runner·Managed Agents·Agent SDK)이 있다. 직접 짜 봤기에 그게 뭘 감추는지 안다.
- 경계: 모델 호출을
LLM인터페이스로 빼면, 루프는 지키고 트랜스포트만 갈아 끼운다. HTTP 없는 fake로 그 이음매를 증명했다. - 네 길: 배움·완전 제어(직접) → 보일러플레이트 덜기(Tool Runner) → 실행 환경까지 맡김(Managed Agents) → 코딩 에이전트 속성(Agent SDK).
- 정직하게: 프로덕션엔 대개 공식을 얹는 게 낫다. 직접 짠 것의 값은 투명성·제어·배움이고, 경계는 어느 쪽이든 남는 자산이다.
시리즈의 큰 뼈대는 여기서 한 번 닫힙니다. 다음에 이어 간다면, 오늘 그은 LLM 경계 위에 실제 공식 SDK 어댑터를 끼워 같은 루프를 돌려 보거나, 프롬프트 캐싱으로 재전송 비용을 줄이는 실전 최적화로 갈 수 있습니다.
핵심 한 줄 — "직접 짜기"와 "맡기기"는 양자택일이 아니다. 원리를 알고 경계를 그으면, 소유할 것(루프·정책)은 지키고 맡길 것(트랜스포트·실행)은 자유롭게 바꾼다. 아키텍처의 이음매는 프레임워크 선택보다 오래간다.