728x90

월말에 Anthropic 사용량 대시보드를 열었다가 "우리가 이걸 이렇게 많이 썼나?" 싶었던 적 있으면 이 글이 도움이 될 거다. Systima에서 Claude Code와 OpenCode를 같은 모델·같은 머신·같은 태스크에 물려놓고, API 경계에서 오가는 요청 페이로드를 전부 뜯어본 실측 리포트를 냈다. 결론부터 말하면 Claude Code는 사용자가 프롬프트를 치기도 전에 약 33,000 토큰을 소비하고, OpenCode는 약 7,000 토큰이었다. 약 4.7배 차이다.

단순히 "Claude Code가 비싸다"로 끝날 얘기가 아니다. 이 오버헤드가 어디서 나오는지, 캐싱은 뭘 살려주고 뭘 못 살려주는지, 그리고 실무에서 어떤 셋업일 때 이게 재앙이 되는지를 알아야 도구를 제대로 고를 수 있다.

1. 왜 지금 이 얘기가 나오는가

AI 코딩 에이전트를 팀에 도입해본 사람이면 알 거다. 처음엔 "토큰? 얼마 안 나오겠지" 하다가, 여러 명이 하루종일 돌리기 시작하면 청구서가 예상 밖으로 튄다. 문제는 토큰 오버헤드가 곧 비용이자 지연시간이자 컨텍스트 예산이라는 점이다.

에이전트가 매 턴마다 보내는 건 여러분의 프롬프트만이 아니다. 시스템 프롬프트, 툴 스키마(도구 정의 JSON), 그리고 각종 주입된 스캐폴딩이 앞에 붙는다. 이 베이스라인은 매 요청마다 다시 전송되거나 캐시에서 다시 읽힌다. 즉 33k 토큰짜리 베이스라인이면, 200k 컨텍스트 윈도우의 약 1/6을 코드 한 줄 넣기도 전에 이미 잡아먹고 시작하는 셈이다.

특히 EU AI Act Article 12처럼 에이전트 동작을 로깅하고 설명할 수 있어야 하는 규제 환경이라면, "내 에이전트가 실제로 뭘 보내는가"를 소문이 아니라 데이터로 답할 수 있어야 한다. 이 리포트가 딱 그걸 한 거다.

2. 33k는 어디서 나오는가 — 시스템 프롬프트 해부

Systima는 하네스(harness, 여기선 Claude Code / OpenCode)와 모델 엔드포인트 사이에 로깅 프록시를 끼워넣었다. 구조는 이렇다.

harness (Claude Code / OpenCode)
    → logging proxy (요청 페이로드 + 응답 usage 캡처)
        → model endpoint

프록시는 두 가지를 기록한다. 하네스가 실제로 내보낸 JSON 페이로드(시스템 블록, 툴 스키마, 메시지)와, API가 반환한 usage 블록(input 토큰, cache write, cache read, output 토큰). 페이로드는 "무엇을 보냈나"의 원본 진실, usage는 "무엇이 과금됐나"의 원본 진실이다.

T1: "OK라고만 답해" — 순수 오버헤드 측정

가장 극단적인 테스트다. 프롬프트는 딱 22자, "Reply with exactly: OK". 여기에 각 하네스가 뭘 얹어 보냈는지 보자.

구성요소 Claude Code OpenCode
시스템 프롬프트 27,344자 (3블록) 9,324자 (1블록)
툴 스키마 27개 툴, 99,778자 10개 툴, 20,856자
첫 메시지 스캐폴딩 7,997자 (system-reminder 블록) 없음
실제 프롬프트 22자 22자
첫 턴 페이로드(보정치) 약 32,800 토큰 약 6,900 토큰

핵심은 두 하네스 모두 툴 스키마가 지배적이라는 점이다. Claude Code의 약 33k 중 약 24,000이 툴 정의고, OpenCode의 약 6,900 중 약 4,800이 툴 정의다.

왜 Claude Code가 이렇게 무거운가? 27개 툴이 코딩 코어만이 아니라 백그라운드 에이전트/오케스트레이션 스위트 전체를 포함하기 때문이다. CronCreate, Monitor, Task 패밀리, worktree 관리, 푸시 알림까지 다 들어있다. 게다가 첫 유저 메시지 앞에 세 개의 리마인더 블록(위임용 에이전트 카탈로그, 사용 가능 스킬 카탈로그, 유저 컨텍스트)이 주입된다.

반면 OpenCode는 거의 미니멀하다. "You are OpenCode, the best coding agent on the planet"으로 시작하는 시스템 블록 하나, 클래식한 코딩 툴 10개, 그리고 여러분의 프롬프트가 유일한 유저 콘텐츠다.

툴을 다 꺼도 여전히 3배

툴을 빼고 시스템 프롬프트 자체만 비교하면, Claude Code는 26,891자(약 6.5k 토큰), OpenCode는 8,811자(약 2.0k 토큰)다. 툴을 전부 제거해도 Claude Code의 명령어 세트가 3배 이상 크다. 나머지는 톤 규칙, 안전 가이드, 태스크 관리 지침, 환경 설명 같은 "행동 강령"이다.

직접 확인하고 싶다면 툴을 끄고 돌려볼 수 있다.

# Claude Code에서 툴 비활성화
claude --tools

# OpenCode에서 툴 비활성화 (설정)
# opencode 설정 JSON에
{ "tools": { "*": false } }

3. 캐싱은 어디까지 살려주나 — 여기가 진짜 함정

"어차피 프롬프트 캐싱 걸리니까 괜찮은 거 아니야?"라고 생각하기 쉽다. 여기가 실무에서 제일 많이 착각하는 지점이다.

OpenCode는 캡처된 모든 실행에서 요청 프리픽스가 바이트 단위로 동일했다. 세션당 한 번 캐시에 쓰고, 이후엔 몇 푼짜리 cache read로 다시 읽는다. 이상적인 캐시 활용이다.

반면 Claude Code는 세션 중간에 수만 토큰의 prompt-cache 토큰을 다시 썼다. 같은 태스크에서 OpenCode보다 최대 54배 많은 cache write를 기록했다. cache write는 프리미엄 가격으로 과금된다. 대시보드 숫자가 치솟는 주범이 바로 이거다.

왜 다시 쓰는가: Claude Code는 대화가 진행되면서 system-reminder 블록을 추가로 주입한다. 첫 턴엔 3개, 첫 툴 라운드트립엔 4개로 늘어난다. 스캐폴딩이 턴 수에 따라 자라니까, 프리픽스가 바뀌고 캐시가 깨지는 것으로 보인다.

중요한 사실 하나. 세 가지는 캐시 할인과 무관하게 무조건 스케일한다.

  • 첫 턴의 cache write (프리미엄 과금)
  • 매 턴의 cache read (싸긴 하지만 0은 아님)
  • 컨텍스트 윈도우 소비 — 이건 어떤 캐시 할인으로도 줄지 않는다

세 번째가 핵심이다. 33k 베이스라인은 매 턴이 200k 윈도우의 1/6 지점에서 시작한다는 뜻이고, 캐싱이 아무리 잘 돼도 이 컨텍스트 압박은 절대 안 줄어든다.

모델을 바꾸면 그림이 달라진다

Systima가 T1을 Claude Fable 5로 재실행했더니 격차가 줄었다. 이유가 흥미롭다. Claude Code의 시스템 프롬프트는 모델 조건부다. Sonnet에는 27,787자를 보냈지만 Fable에는 10,526자만 보냈고, 툴 스키마도 99,778자에서 82,283자로 줄었다. 같은 27개 툴인데 doctrine이 훨씬 적다. OpenCode 페이로드는 두 모델에서 바이트 단위로 동일했다.

결과적으로 페이로드 기준 격차가 Sonnet에서 4.7배, Fable에서 약 3.3배로 좁혀졌다. 여전히 훨씬 무겁지만, 배수는 모델에 따라 다르다는 점을 기억하자.

4. 실무 관점 — 멀티플라이어와 흔한 함정

T1은 최선의 시나리오다. 현실 세션은 저렇게 린하게 시작해서 짧게 끝나지 않는다. 실무에서 얹히는 레이어들을 보자.

멀티플라이어 1: 지시 파일 (AGENTS.md / CLAUDE.md)

프로덕션 레포의 72KB 지시 파일을 워크스페이스에 넣고 T1을 재실행하니, 양쪽 다 요청당 20,000 토큰 이상이 추가됐다.

  • OpenCode: 13,152 → 33,336 토큰
  • Claude Code: 39,005 → 59,243 토큰

여기 첫 번째 함정. Claude Code 2.1.207은 AGENTS.md를 완전히 무시했다. CLAUDE.md로 이름을 바꿔야만 읽었고, 그것도 시스템 프롬프트가 아니라 첫 유저 메시지에 주입했다. OpenCode는 두 파일명을 다 읽고 시스템 프롬프트에 넣는다.

이게 왜 무서운가 하면, 무시된 지시 파일은 아무 에러도 안 낸다. 팀 컨벤션을 열심히 적어놨는데 에이전트가 조용히 무시하고 있는 상황을 상상해보라. 파일명을 잘못 쓰면 이런 식으로 조용히 아무 일도 안 일어난다.

# 실무 체크: 하네스가 어떤 파일명을 실제로 먹는지 확인
$ ls -la
-rw-r--r--  1 dev  staff  73728  AGENTS.md   # Claude Code가 무시함!

# 로그를 보면 지시 파일 관련 주입이 아예 없다
# (에러 메시지가 안 나오는 게 오히려 함정)

# 해결: CLAUDE.md로 심볼릭 링크 또는 리네임
$ ln -s AGENTS.md CLAUDE.md
$ ls -la
lrwxr-xr-x  1 dev  staff      9  CLAUDE.md -> AGENTS.md
-rw-r--r--  1 dev  staff  73728  AGENTS.md

멀티플라이어 2: MCP 서버

MCP 서버를 붙이면 스키마가 프롬프트에 추가된다. 스키마는 하네스 간 동일하니 세금도 거의 같다. 작은 서버당 요청당 약 1,000~1,400 토큰. 5개 서버를 붙였더니 Claude Code는 페이로드 기준 4,900 토큰, OpenCode는 metered 6,967 토큰이 늘었고, 툴 개수가 각각 27→69, 10→52로 불었다.

여기 두 번째 조용한 함정. Claude Code는 print 모드에서 프로젝트 스코프 .mcp.json을 조용히 무시했다. --mcp-config 플래그를 명시해야 읽었다. 서버가 붙어있다고 가정했는데 실제론 안 붙어있는 상황이 생긴다.

# .mcp.json이 있는데도 print 모드에서 서버가 안 잡히는 경우
$ claude -p "list available tools"
# → MCP 툴이 목록에 안 보임 (에러도 없음)

# 명시적으로 config를 넘겨야 함
$ claude -p "list available tools" --mcp-config .mcp.json
# → 이제 MCP 서버 툴이 스키마에 포함됨

공교육용 교훈은 하나다. 서버가 붙었다고 가정하지 말고 경계(API boundary)에서 검증하라. 프록시 로그든 툴 목록이든, 실제 페이로드를 눈으로 확인해야 한다.

멀티플라이어 3: 프레임워크 템플릿

BMAD 같은 스토리 기반 워크플로우 프레임워크는 슬래시 커맨드를 페르소나·프로토콜·체크리스트로 가득한 큰 템플릿으로 확장한다. 8,405자짜리 대표 템플릿을 T3 프롬프트로 넣어봤더니, 템플릿 자체는 약 2,100 토큰이지만 대화 히스토리에 들어가서 이후 모든 요청이 다시 실어 나른다. 9-요청 세션이면 9번 재전송된다. 프레임워크 세금 = 템플릿 크기 × 요청 수, 그리고 이건 위의 모든 것 위에 쌓인다.

멀티플라이어 4: 서브에이전트 — 여기서 총액이 폭발

이게 제일 무섭다. 작업을 서브에이전트 2개로 팬아웃했더니, 직접 하면 121,000 토큰이던 작은 태스크가 513,000 토큰으로 뛰었다. 서브에이전트마다 자기 부트스트랩 비용이 있고, 부모가 다시 그 트랜스크립트를 소비하기 때문이다. "병렬로 나누면 빠르겠지"라고 서브에이전트를 남발하면 토큰 청구서가 4배 넘게 튀는 걸 각오해야 한다.

반전: 멀티스텝에선 Claude Code가 더 쌀 수도 있다

모든 게 Claude Code 불리로만 흐르는 건 아니다. T3(write-run-test-fix 루프)에서는 예상이 뒤집혔다.

지표 Claude Code OpenCode
모델 요청 수 3 9 (+타이틀 콜 1)
툴 호출 방식 한 라운드트립에 병렬 배치 턴당 툴 1개
누적 metered input 약 121,000 토큰 약 132,000 토큰

Claude Code는 전체 작업(파일 쓰기 2개 + 스크립트 실행 2개)을 단일 병렬 툴 라운드트립으로 배치했다. OpenCode는 턴당 정확히 툴 하나씩 9번 돌았다. 베이스라인은 매 요청마다 재전송되므로 요청 수가 베이스라인을 곱한다. OpenCode는 약 7k 베이스라인을 9번 냈고, Claude Code는 약 33k를 3번 냈다. 결과적으로 총액이 수렴했다.

공식: 전체 작업 input ≈ 베이스라인 × 요청 수 + 대화 성장분. 큰 베이스라인 + 공격적 배칭 하네스와, 작은 베이스라인 + 직렬 하네스가 같은 지점에 착지할 수 있다. 미터는 높은 데서 시작하지만, 세션이 어떻게 전개되느냐가 누가 더 쓸지를 결정한다.

5. 정리 — 누가 언제 뭘 써야 하나

한 줄 요약: Claude Code는 시작 오버헤드가 크지만 툴 배칭이 강하고, OpenCode는 베이스라인이 가볍지만 턴마다 그 비용을 반복 지불한다.

  • 짧고 단순한 반복 작업이 많다면 (커밋 메시지, 소규모 리팩터, 단발성 질의): OpenCode가 유리하다. 33k를 매번 태울 이유가 없다.
  • 복잡한 멀티스텝 작업을 배치로 처리한다면: Claude Code의 병렬 배칭이 요청 수를 줄여 총액을 낮출 수 있다.
  • 컨텍스트 윈도우가 빡빡한 대형 코드베이스라면: 캐시 할인과 무관하게 33k가 윈도우를 잡아먹는다는 걸 기억하라. 여기선 베이스라인이 가벼운 쪽이 실질적으로 유리하다.
  • 규제 환경(EU AI Act 등)이라면: 로깅 프록시를 경계에 두고 실제 페이로드를 캡처하라. "무엇을 보내는가"를 데이터로 답할 수 있어야 한다.

그리고 도입 전 반드시 확인할 것 세 가지: ① 지시 파일 파일명을 하네스가 실제로 먹는지(AGENTS.md vs CLAUDE.md), ② MCP 서버가 경계에서 진짜 붙었는지, ③ 서브에이전트 팬아웃이 정말 필요한지(4배 폭발 각오). 이 셋만 챙겨도 예상 밖 청구서는 크게 줄어든다.

마지막으로, 이 모든 수치는 특정 버전(Claude Code 2.1.207, OpenCode 1.17.18, 2026년 7월 claude-sonnet-4-5 / claude-fable-5 기준)에서 나온 것이다. 하네스와 모델은 계속 바뀌므로, 여러분 환경에서는 직접 프록시를 끼워 측정하는 게 정답이다. 소문 말고 데이터로.

참고 자료

728x90
728x90

도입: 200만 토큰이라는 숫자에 속지 마라

벤더들이 "컨텍스트 창 1M, 2M" 하고 광고하는 걸 보면, 이제 RAG 파이프라인에서 청킹 같은 거 신경 안 써도 되는 거 아닌가 싶은 생각이 든다. 그냥 문서 통째로 다 때려넣으면 알아서 잘 답해주겠지, 라고. 실무에서 한 번이라도 긴 세션을 굴려본 사람이라면 그게 착각이라는 걸 안다.

핵심은 간단하다. 광고되는 컨텍스트 창 크기 = 실제로 모델이 똑똑하게 쓸 수 있는 범위가 아니다. 원문(큰 컨텍스트 창을 신뢰하지 마라)에서 짚는 건 컨텍스트 창이 두 구간으로 나뉜다는 점이다. 모델이 예리하게 작동하는 "스마트 구간"과, 앞에서 한 지시를 슬슬 까먹기 시작하는 "둔한 구간". 그 경계가 대략 100k 토큰 부근이라는 얘기다.

이게 왜 지금 중요하냐면, 요즘 코딩 에이전트(Claude Code, Cursor 등)는 토큰을 미친 듯이 소모한다. 파일 몇 개 읽고, 긴 디버깅 세션 한 번 돌리고, 테스트 전체 한 번 실행하면 100k는 우습게 넘긴다. 즉 우리가 일상적으로 굴리는 작업이 자기도 모르게 "둔한 구간"에 진입한다는 거다.

핵심: 100k 임계점과 둔한 구간의 정체

스마트 구간 vs 둔한 구간

비유를 하나 들자면, 회의에 들어가서 처음 10분은 다들 집중해서 안건을 또렷이 기억한다. 그런데 두 시간짜리 회의가 끝나갈 무렵엔 "아 그래서 아까 그 결정이 뭐였죠?" 하는 사람이 나온다. LLM도 비슷하다. 컨텍스트 창 앞쪽 일부 청크는 또렷하게 처리하지만, 창을 채워나갈수록 주의력이 분산되면서 성능이 점진적으로 떨어진다.

원문에서 인용하는 RULER 벤치마크와 Chroma의 context rot 보고서가 이걸 데이터로 보여준다. 요지는 유효 컨텍스트(effective context)는 광고된 숫자의 일부에 불과하고, 창을 채울수록 성능이 떨어진다는 것. 단, 원문에서도 명시하듯 이 연구들은 2024~2025년 자료라 최신 Claude 모델에 그대로 적용된다고 보긴 어렵다. 모델 버전마다 어텐션 구조와 장기 컨텍스트 학습량이 다르기 때문이다. 그래서 "100k"라는 숫자도 절대값이라기보단 경험적 가이드라인으로 받아들이는 게 맞다.

왜 잊어버리는가 — Attention 관점

Transformer의 self-attention은 입력 토큰 전부에 대해 서로의 관련도를 계산한다. 토큰이 N개면 attention 계산은 대략 N² 스케일로 늘어난다. 문제는 토큰이 많아질수록 "지금 이 작업에 진짜 중요한 토큰"에 쏠려야 할 주의력이, 관련 없는 수많은 토큰들과 나눠 가지게 된다는 점이다.

원문 HN 댓글 중에 이걸 잘 짚은 게 있다. "컨텍스트 창 안에서 어떤 것이 자주 등장하면, 설령 틀린 것이어도 가중치가 생긴다." 즉 디버깅 세션에서 실패한 접근법이 컨텍스트에 계속 쌓이면, 모델이 그 실패한 접근을 자꾸 다시 시도하는 현상이 생긴다. 절대적인 컨텍스트 크기보다 창 안에 찌꺼기와 잘못된 방향 지시가 쌓여서 정작 중요한 내용을 덮어버리는 것이 더 큰 문제라는 지적이다. 이거 실무에서 정말 자주 겪는다.

실무 관점: 어디서 터지고, 어떻게 막는가

흔한 함정 1 — 코딩 에이전트가 둔한 구간에서 압축을 시작한다

Claude Code의 auto-compact가 대표적이다. 세션이 길어지면 기록을 요약하고 새로 시작하는 기능인데, 함정은 이미 둔한 구간에 들어간 다음에 작동한다는 점이다. 게다가 요약을 만드는 모델 자체가 이미 성능이 저하된 상태다. 멍청해진 모델이 만든 요약을 들고 다음 세션을 시작하니, 인계 품질이 떨어진다.

실제로 긴 세션에서 이런 메시지를 만나게 된다:

Context low (8% remaining) · Run /compact to compact & continue

이게 뜨기 전에 이미 작업 품질이 떨어지고 있었다는 게 핵심이다. 그리고 도구 호출이 실패하기 시작하면 이런 것도 본다(에이전트/하네스마다 메시지는 다르다):

Error: Input is too long for requested model.
prompt token count (215431) exceeds the maximum (200000)

이 에러를 만났다면 이미 한참 전부터 모델이 흐리멍덩해진 상태로 작업하고 있었다고 봐야 한다.

흔한 함정 2 — "메모리 시스템"이 오히려 모델을 멍청하게 만든다

원문 댓글에서 강하게 동의가 모인 지점이다. "모델에는 메모리가 있는 게 아니라 컨텍스트만 있다." 관련 없는 사실을 메모리랍시고 컨텍스트에 계속 밀어 넣으면, 정작 현재 문제에 쓸 컨텍스트가 줄어든다. 방해가 적을수록 결과가 좋아진다는 것.

한 댓글은 이런 웃픈 상황도 공유한다 — Opus가 메모리에 뭔가 계속 써놓긴 하는데, 정작 같은 실수를 반복하기 전에 그 메모리를 확인하는 걸 까먹는다. 그래서 "메모리 확인하라고 기억해!"가 또 메모리로 저장된다. 잘 작동하는 시스템이 아니라는 거다.

대응 전략 1 — written artifact로 정보를 세션 밖에 빼라

가장 실용적인 처방은 자동 요약에 기대지 말고, 사람이 직접 쓴 명세(spec)를 다음 세션에 인계하는 것이다. 직접 쓴 명세는 자동 요약보다 신호가 강하다. 무엇이 중요한지 사람이 직접 결정했기 때문이다.

예를 들어 작업 시작 전에 이런 식으로 PRD나 계획을 Markdown으로 남기고 저장소에 커밋한다:

# 작업 명세: 결제 모듈 리팩토링

## 목표
- PaymentService의 동기 호출을 비동기 큐 기반으로 전환

## 이미 시도했고 실패한 접근 (다시 하지 말 것)
- Redis Pub/Sub 직접 사용 → at-least-once 보장 안 됨
- 트랜잭션 내부에서 큐 발행 → 롤백 시 유령 메시지 발생

## 확정된 방향
- Transactional Outbox 패턴 사용
- 편집 대상: src/payment/outbox.ts, src/payment/PaymentService.ts

## 체크리스트
- [ ] Outbox 테이블 마이그레이션
- [ ] Outbox poller 구현
- [ ] 기존 동기 호출 제거

핵심은 "실패한 접근" 섹션이다. 이걸 명시해두면 새 세션에서 모델이 같은 실패를 반복하는 걸 막는다. 이게 원문에서 말하는 breadcrumb(빵부스러기) 접근이다 — 다음 세션이나 다음 사람이 깔끔하게 이어받을 산출물을 남기는 것.

대응 전략 2 — 메인 스레드는 조율만, 무거운 작업은 하위 에이전트로

원문 댓글 중 가장 인사이트 있던 기법이다. 사용자와의 최상위 대화 스레드에서는 도구 호출을 막고, 토큰을 잡아먹는 작업(파일 읽기, 데이터 수집, 테스트 실행)은 하위 에이전트의 재귀 호출 안에서만 돌린 뒤 요약된 결과만 메인 스레드로 반환하는 방식이다.

이렇게 하면 하위 에이전트가 5천만 토큰을 태워도 루트 대화 스레드는 10만 토큰도 안 건드릴 수 있다. 100만 LOC 코드베이스에서도 메인 대화는 스마트 구간에 계속 머무른다. 개념을 의사코드로 표현하면:

# 안티패턴: 모든 걸 평면 컨텍스트에 다 들고 다님
main_context = []
main_context += read_file("a.ts")      # +20k 토큰
main_context += read_file("b.ts")      # +30k 토큰
main_context += run_tests()            # +50k 토큰
# → 메인 스레드가 순식간에 둔한 구간으로

# 권장: 메인은 조율, 무거운 작업은 격리
def main_agent(task):
    # 도구 호출 금지. sub_agent 결과 요약만 받음
    summary = sub_agent(task="b.ts 분석 후 핵심만 보고")
    # summary는 2~3k 토큰. 메인은 스마트 구간 유지
    return decide_next_step(summary)

Claude Code에서는 이걸 부분적으로 자동으로 한다. "컨텍스트 많이 먹겠다" 싶은 휴리스틱이 작동하면 하위 에이전트(subagent)로 넘기는 식이다. 데이터 수집·집계를 하위 에이전트에 던지고 요약만 꺼내오는 패턴을 자주 볼 수 있다.

다만 트레이드오프도 있다. 원문 댓글에서 솔직하게 짚듯 재귀 깊이 1을 넘어서면 실제 성능 이득을 보기 어렵다. 외부의 기호적 재귀는 최전선 모델이 학습한 대상이 아닌 듯하다는 관찰이다. 그리고 하위 에이전트가 매번 "부트스트랩"하는 재작업 비용이 든다. 그래도 거대한 평면 컨텍스트를 들고 다니는 것보단 훨씬 효율적이라는 게 중론이다.

RAG 파이프라인이라면

RAG 운영자 입장에서 시사점은 분명하다. "컨텍스트 창 커졌으니 청킹/리랭킹 대충 해도 된다"는 유혹을 거부해야 한다. 검색해온 청크를 무작정 많이 넣을수록, 관련 없는 청크가 정작 중요한 청크의 주의력을 빼앗는다. top_k를 늘리는 게 항상 정답이 아니다. 리랭커로 정말 관련 높은 소수의 청크만 추리고, 컨텍스트를 예산처럼 다루는 게 맞다. 도움이 되는 부분은 앞쪽 일부 청크라고 가정하고 설계하는 편이 안전하다.

정리: 무엇을 봐야 하는가

한 줄 요약: 컨텍스트 창 크기는 마케팅 숫자에 가깝다. 실제로 봐야 할 건 "유효 컨텍스트를 스마트 구간 안에 유지하는 워크플로"다.

  • 코딩 에이전트를 굴리는 사람: 긴 세션을 무작정 이어가지 말고, 작업 단위마다 직접 쓴 명세(특히 "실패한 접근" 기록)를 남기고 새 세션으로 인계하라. auto-compact는 보험이지 전략이 아니다.
  • RAG 파이프라인 운영자: 컨텍스트 창이 커졌다고 청킹/리랭킹을 느슨하게 하지 마라. top_k는 신중하게. 관련 없는 청크는 노이즈다.
  • 에이전트 설계자: 메인 대화는 조율자로, 토큰 먹는 작업은 하위 에이전트로 격리하라. 단 재귀 깊이는 1로 제한하는 게 현실적이다.

마지막으로 균형을 위해 덧붙이면 — 원문 댓글에는 "최신 Opus(예: 4.8)에서는 50만 토큰을 편하게 넘긴 세션도 있었다"는 반론도 있다. 모델 버전, 하네스, 작업 종류에 따라 체감 임계점은 크게 다르다. 그러니 "100k"를 신앙처럼 외우지 말고, 본인 환경에서 어느 지점부터 회상 실수가 시작되는지 직접 로깅해서 감을 잡는 것이 가장 확실하다. 결국 컨텍스트 엔지니어링은 아직 다들 숙련도만 다른 땜장이 단계라는 자조 섞인 댓글이, 어쩌면 가장 정직한 현실 인식일지도 모른다.

참고 자료

728x90
728x90

며칠 전 사내 슬랙에 "내 노트북 메모리가 켜기만 해도 60% 넘게 먹는데 뭐가 문제냐"는 질문이 올라왔다. 범인은 Claude Desktop이었다. 채팅 한 줄 안 쳤는데도 Task Manager에 Vmmem 프로세스가 1.8GB를 잡아먹고 있었다. 이게 단순 버그가 아니라 데스크톱 AI 툴의 샌드박스 아키텍처가 어떻게 굴러가는지 보여주는 좋은 케이스라서, 실무 관점에서 한번 정리해본다.

1. 왜 지금 화제인가 — 채팅만 쓰는데 VM이 뜬다

이슈의 핵심은 단순하다. Claude Desktop Windows 앱이 Cowork(agent mode)를 한 번이라도 쓴 뒤로는, 그냥 채팅만 하려고 앱을 열어도 매번 Hyper-V VM을 띄운다는 거다. 재현 환경은 다음과 같이 보고됐다.

  • Windows 11 Pro 25H2 (Build 26200.7840)
  • Hyper-V, WSL, Docker, Windows Sandbox 전부 비활성화 상태
  • 단, VirtualMachinePlatform 기능은 켜져 있음
  • Core Isolation / Memory Integrity도 꺼져 있는 상태

여기서 재밌는 포인트는, 보고자가 wsl --shutdown을 쳐도 "not installed"가 나오고 Get-VM은 실패하는데도 VM이 뜬다는 점이다. 즉 사용자가 흔히 아는 Hyper-V 관리 인터페이스로는 잡히지 않는 경로로 VM을 생성한다는 뜻이다.

왜 VM을 띄우느냐 자체는 명확하다. Cowork/agent mode는 Claude가 사용자 머신에서 실제로 명령을 실행하고 파일을 건드리는 기능인데, 호스트를 직접 건드리게 하면 위험하니까 격리된 샌드박스 안에서 돌린다. MCP(Model Context Protocol) 기반 툴이 코드를 실행할 때 호스트 오염을 막으려는 설계다. 이 방향성 자체는 맞다. 문제는 채팅 전용으로 쓸 때도 VM을 미리 띄우고, 끌 방법을 안 준다는 것이다.

2. 동작 원리 — VirtualMachinePlatform과 Vmmem의 정체

먼저 용어 정리부터 하자. 현장에서 Hyper-V랑 VirtualMachinePlatform을 같은 거라고 착각하는 경우가 많은데 다르다.

  • Hyper-V Platform: 전통적인 Type-1 하이퍼바이저. Get-VM, Hyper-V Manager로 관리하는 그 풀스택 가상화 기능이다.
  • VirtualMachinePlatform (VMP): WSL2, Windows Sandbox, Docker Desktop 등이 쓰는 경량 유틸리티 VM 기반 기능. Hyper-V Manager에는 안 잡히지만 내부적으로는 같은 가상화 스택(vmcompute)을 쓴다.

이게 핵심이다. 보고자가 Hyper-V를 껐는데도 VM이 뜬 이유는, Claude가 Hyper-V가 아니라 VMP의 vmcompute(Host Compute Service)를 직접 트리거하기 때문이다. 보고 내용을 보면 프로세스 트리가 이렇게 나온다.

Claude Desktop
  └─ RPC interface event → vmcompute (Host Compute Service)
       └─ vmwp.exe (VM Worker Process, 부모: services.exe)
            └─ Vmmem (게스트 메모리 영역, ~1,796~1,846MB)

Vmmem은 별도 프로그램이 아니라 VM 게스트에 할당된 메모리를 호스트 Task Manager에서 보여주기 위한 가상 프로세스다. WSL2를 써본 사람이면 익숙할 거다. WSL2 게스트가 메모리를 먹으면 그게 Vmmem으로 잡힌다. 즉 Claude가 띄운 유틸리티 VM의 메모리 풋프린트가 Vmmem 1.8GB로 나타나는 것이다.

비유하자면, 컨테이너 하나만 돌리려고 Docker Desktop을 켰는데 컨테이너를 다 지워도 백그라운드 LinuxKit VM은 계속 메모리를 잡고 있는 상황과 똑같다. 다른 점은 Docker는 "내가 VM 띄웠다"고 명시적으로 알려주는데, Claude는 채팅만 쓰는 사용자한테 아무 안내 없이 조용히 띄운다는 점이다.

3. 실무 관점 — 측정, 흔한 함정, 대응 전략

실제로 뭐가 도는지 확인하기

먼저 본인 머신에서 진짜 VMP VM이 떠 있는지 확인하는 명령어다. 관리자 PowerShell에서 실행한다.

PS C:\> Get-Process vmwp, vmcompute -ErrorAction SilentlyContinue |
>>   Select-Object Name, Id, @{N='RAM(MB)';E={[math]::Round($_.WorkingSet64/1MB,0)}}

Name        Id  RAM(MB)
----        --  -------
vmcompute  4820       18
vmwp       9132     1812

vmwp가 1800MB 근처를 잡고 있으면 Claude가 띄운 유틸리티 VM이 살아있다는 신호다. Get-VM이 실패하는 환경이라도 이 프로세스는 잡힌다. Hyper-V 관리 cmdlet과는 다른 레이어라는 점을 다시 강조해둔다.

VMP 기능 자체가 켜져 있는지는 이렇게 본다.

PS C:\> Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform |
>>   Select-Object FeatureName, State

FeatureName             State
-----------             -----
VirtualMachinePlatform Enabled

흔한 함정 — 세션 파일 누적과 JSON 에러

보고에서 주목할 부분이 두 가지 있다. 첫째, %APPDATA%\Claude\local-agent-mode-sessions\ 안에 이전 Cowork 세션 파일이 2,689개 쌓여 있었다는 점이다. 세션 종료 후 정리 로직이 없다는 뜻이다. 더 황당한 건 이 파일들을 다 지우고 VM 프로세스를 죽여도, 앱을 다시 켜면 VM과 1.8GB Vmmem이 즉시 다시 생긴다는 점이다. 세션 파일이 원인이 아니라 앱 자체가 시작 시 무조건 VM을 띄우는 구조라는 거다.

둘째, Hyper-V Compute Admin 로그(이벤트 뷰어 → Applications and Services Logs → Microsoft → Windows → Hyper-V-Compute)에 부팅·앱 실행 때마다 이런 에러가 반복된다.

The specified property query is invalid:
The virtual machine or container JSON document is invalid.
(0xC037010D, 'Invalid JSON document '$'')

0xC037010D 에러는 vmcompute에 넘긴 VM 정의 JSON이 깨졌을 때 나는 거다. 빈 $ 문자열을 JSON으로 던진 흔적인데, 이게 매 실행마다 찍히는 걸 보면 VM 초기화 로직이 정상 경로를 안 타고 있을 가능성이 높다. 이벤트 로그가 이런 에러로 도배되면 모니터링 알림 노이즈가 늘어나니, 사내 SIEM에 Hyper-V-Compute 로그를 물려둔 곳이라면 미리 필터링 룰을 잡아두는 게 좋다.

리소스 영향

보고 기준으로 16GB 시스템에서 유휴 메모리 사용량이 약 50%에서 62%로 올라갔고, 일반 앱 부하가 겹치면 70~75%까지 치솟았다. 16GB 노트북에서 1.8GB를 상시 떼이는 건 무시 못 할 수준이다. 특히 VDI나 회의실 공용 PC처럼 메모리가 빠듯한 환경에서는 체감이 크다. (디스크의 경우 macOS에서는 약 10GB VM 번들을 만든다는 별도 보고가 있는데, Windows 쪽 디스크 사용량 수치는 본 보고에 명시돼 있지 않다. 환경별 확인이 필요하다.)

대응 전략

(1) Cowork를 안 쓴다면 — VMP 자체를 끈다

PS C:\> Disable-WindowsOptionalFeature -Online `
>>   -FeatureName VirtualMachinePlatform -NoRestart

Path          :
Online        : True
RestartNeeded : Possible

가장 확실한 방법이지만 부작용이 있다. WSL2, Docker Desktop, Windows Sandbox도 같이 못 쓰게 된다. 개발 머신에서 WSL2를 쓰고 있다면 이 옵션은 못 쓴다. 그리고 당연히 Cowork 기능도 비활성화된다.

(2) VMP는 살리되 VM만 매번 죽이기

PS C:\> Stop-Process -Name vmwp -Force
PS C:\> Stop-Process -Name vmcompute -Force

이렇게 죽여도 채팅 기능은 정상 동작한다. 다만 앱 재실행 때마다 다시 떠서 반복 작업이 된다. 매번 손으로 치기 귀찮으면 작업 스케줄러에 등록하거나, Claude 실행을 감싸는 래퍼 스크립트로 후처리하는 식으로 자동화할 수 있다. 단 vmcompute는 다른 가상화 기능도 공유하는 서비스라, WSL2 등을 같이 쓰는 머신에서는 vmcompute까지 죽이면 다른 VM도 영향을 받을 수 있으니 주의해야 한다.

(3) 격리 VM 안에서 Claude를 돌린다

HN 댓글에서 가장 깔끔한 해법으로 언급된 방식이다. Claude Desktop을 Windows Sandbox나 별도 Hyper-V VM 안에서 돌리고, 그 게스트 VM 안에는 VirtualMachinePlatform을 설치하지 않는 것이다. 그러면 Claude가 VMP가 없는 걸 감지하고 Cowork 탭을 그냥 비활성화한다. 실제로 "VMP가 전혀 설치 안 된 VM에서 돌리니 앱이 이를 받아들이고 Cowork를 비활성화하더라"는 보고가 있다.

다만 이건 트레이드오프가 있다. 기업 환경에서 VM 안에서 도구를 돌리면 관측 가능성(observability)이 떨어진다. 플랫폼 담당자나 보안팀 입장에선 사용자 수준 텔레메트리와 샌드박스 내부 로그가 분리되는 게 골치 아픈 지점이다. EDR(Defender, CrowdStrike 등)이 게스트 내부를 어떻게 볼지도 따로 정책을 잡아야 한다.

(4) 그냥 웹/PWA를 쓴다

냉정하게 말하면, 컴퓨터에 아무것도 접근시키지 않고 채팅만 할 거라면 데스크톱 앱을 쓸 이유가 별로 없다. HN에서도 "빠른 질문은 Claude 웹 앱을 PWA로 고정해서 쓰고, 프로젝트 작업은 CLI를 쓴다"는 운영 패턴이 여러 번 언급됐다. 채팅 전용 사용자라면 PWA가 메모리 풋프린트 측면에서 압도적으로 가볍다.

4. 정리 — 누가 언제 신경 써야 하나

한 줄 요약: Claude Desktop은 Cowork를 한 번 쓰면 그 뒤로는 채팅만 해도 시작 시 VMP 기반 유틸리티 VM을 띄워 Vmmem으로 ~1.8GB를 상시 점유하며, 현재 공식적으로 이걸 끌 토글은 없다.

  • WSL2/Docker를 안 쓰고 채팅 위주라면: VMP를 끄거나 그냥 PWA로 갈아타라. 제일 깔끔하다.
  • WSL2/Docker를 같이 쓰는 개발 머신이라면: VMP를 못 끄니, vmwp 종료 자동화나 격리 VM 방식을 고려하라.
  • VDI·공용 PC·메모리 빠듯한 환경을 운영한다면: 배포 정책에서 Claude Desktop을 통제하거나, 표준 이미지에서 Cowork 사용을 막는 가이드를 미리 만들어둬라. 1.8GB 상시 점유는 동시 세션 밀도에 직접 영향을 준다.

방향성 자체는 데스크톱 AI 툴의 자연스러운 흐름이다. 에이전트가 호스트에서 명령을 실행하려면 샌드박스가 필수고, 앞으로 나올 도구들도 대부분 비슷한 구조를 갖게 될 거다. 다만 이번 케이스의 진짜 교훈은 "기능을 안 쓰는 사용자에게는 비용을 물리지 말라"는 기본 원칙이다. 요청된 동작도 결국 "Cowork가 실제로 요청될 때만 VM을 초기화하고, 세션 종료 후 정리하고, 필요 없으면 채팅 전용 모드로 가라"는 지극히 상식적인 내용이다. 향후 버전에서 lazy initialization이 들어가는지 릴리스 노트를 지켜볼 필요가 있다.

참고 자료

728x90

+ Recent posts