이번 시간에는 Claude Code 세션을 이어갈지 새로 시작할지를 claude -p 결과 JSON의 토큰 숫자로 비교하며 배워가는 시간입니다.
세션을 길게 유지하면 편하죠. 이전에 뭘 했는지 다시 설명할 필요가 없으니까요. 대신 관련 없는 이력까지 계속 따라옵니다.
그래서 흔히 하는 조언이 "작업마다 세션을 새로 시작해라"인데요. 직접 재 보면 이 조언은 반쯤만 맞아요.
이어가기와 새로 시작을 직접 재봅니다
테스트 설계는 이렇게 할게요. 먼저 작업 A를 하나 시킵니다. 그다음 A와 다른 파일을 보는 작업 B를 두 가지 방식으로 시켜요. A 세션을 이어서 한 번, 새 세션에서 한 번이요.
세션을 정확하게 제어해야 하니까 headless로 돌렸어요. headless는 대화 화면 없이 claude -p "질문"으로 한 번 실행하고 결과만 받는 방식이에요.
옵션은 세 가지를 붙였습니다.
--safe-mode: CLAUDE.md, 스킬, 플러그인, 훅, MCP 서버 같은 개인 설정을 끄고 엽니다. 내 설정이 결과에 섞이지 않게 하려고 붙였어요.--output-format json: 결과를 토큰 사용량이 담긴 JSON으로 돌려줍니다.--max-turns 6: 이번 실행에서 모델과 주고받는 왕복을 6번까지로 묶어요.
작업 A는 이렇게 실행했어요.
claude --safe-mode -p "app/service.py의 WIP_LIMIT 값이 뭐야?" \
--output-format json --max-turns 6 > a.json
결과 JSON의 session_id가 이 세션의 아이디예요. --resume에 세션 아이디를 주면 그 세션의 이력을 달고 이어서 실행합니다. 그래서 아이디를 변수에 넣고, 같은 질문 B를 두 방식으로 실행했어요.
SID="$(jq -r .session_id a.json)"
claude --safe-mode -p --resume "$SID" "repository.py에는 함수가 몇 개야?" \
--output-format json --max-turns 6 > b-resume.json
claude --safe-mode -p "repository.py에는 함수가 몇 개야?" \
--output-format json --max-turns 6 > b-fresh.json
결과 JSON에서 어디를 봐야 할까요
두 파일에서 볼 곳은 usage의 세 항목이에요.
input_tokens: 캐시와 상관없이 그대로 처리한 입력cache_creation_input_tokens: 이번에 캐시에 새로 쓴 입력cache_read_input_tokens: 캐시에서 읽어 온 입력
여기서 캐시는 프롬프트 캐시예요. 요청의 앞부분이 앞선 요청과 글자 하나까지 같으면, Claude는 그 부분을 다시 처리하지 않고 저장해 둔 결과를 읽어 옵니다. 처음 저장하는 걸 캐시 쓰기, 다시 꺼내 쓰는 걸 캐시 읽기라고 불러요.
세 값을 더한 게 그 실행의 입력 합이에요. 한 번 실행하는 동안 모델과 여러 번 주고받았다면 그 요청들이 모두 더해진 값이라서, 요청 하나의 크기로 읽으면 안 됩니다.
num_turns도 같이 보시면 좋아요. 이번 실행 안에서 모델과 주고받은 왕복 수라서, 입력이 왜 그만큼 나왔는지 짐작하는 데 도움이 됩니다.
그런데 total_cost_usd를 맞대 보면 이상한 게 보일 수 있어요. 입력이 작은 이어받은 쪽이 오히려 더 비싸게 찍히기도 하거든요.
이 값이 이번 실행 하나의 비용이 아니라서 그래요. --resume이나 --continue로 이어받은 실행의 total_cost_usd는 그 세션에 지금까지 들어간 비용을 모두 더한 값이에요(2026-10 문서 기준, Claude Code 2.1.277 이후). 이어받은 쪽에는 작업 A의 비용이 같이 들어 있고, 새로 시작한 쪽에는 이번 실행분만 있습니다. 게다가 이 값은 API 단가로 계산한 추정치예요.
아래쪽 modelUsage도 세션 누적이라서, 캐시 읽기 값이 작업 A 때 값에 이번 값을 더한 수치로 나와요.
그래서 실행 하나끼리 비교할 수 있는 건 usage의 세 항목과 num_turns뿐이에요.
이력을 달고 간 쪽이 더 쌌어요
이 부분은 제가 실측한 결과를 기반으로 알려드릴게요. 기록해 둔 usage 세 값을 결과 JSON과 같은 모양으로 옮기면 이렇습니다. 먼저 이어서 실행한 b-resume.json이에요(발췌).
{
"usage": {
"input_tokens": 4,
"cache_creation_input_tokens": 337,
"cache_read_input_tokens": 43954
}
}
새로 시작한 b-fresh.json은 이랬어요(발췌).
{
"usage": {
"input_tokens": 6,
"cache_creation_input_tokens": 3432,
"cache_read_input_tokens": 62258
}
}
세 값을 더해 볼게요.
- 이어서 실행: 4 + 337 + 43,954 = 44,295토큰
- 새로 시작: 6 + 3,432 + 62,258 = 65,696토큰
이력을 달고 간 쪽이 65,696 - 44,295 = 21,401토큰 적었어요. 예상과 반대죠?
다만 조건마다 한 번씩만 돌린 결과예요. Claude가 파일을 찾는 경로는 실행마다 달라질 수 있어서, 다시 돌리면 차이의 크기가 달라지고 경우에 따라 방향도 바뀔 수 있어요. 이 결과로 말할 수 있는 건 "새 세션이 늘 싸다"가 이 경우에는 맞지 않았다는 데까지입니다.
왜 이렇게 나왔을까요? 이유는 두 가지로 볼 수 있어요.
첫 번째는 캐시예요. 캐시 쓰기를 보시면 이어간 쪽은 337토큰, 새로 시작한 쪽은 3,432토큰이에요. 이어간 쪽은 A를 하면서 캐시에 써 둔 이력을 거의 그대로 읽었고, 새로 시작한 쪽은 새 대화를 캐시에 쓰면서 시작했어요.
새로 시작한 쪽의 캐시 읽기도 0이 아니에요. 시스템 프롬프트와 도구 정의처럼 실행마다 똑같이 앞에 붙는 부분은, 따로 시작한 실행이라도 글자가 같으면 캐시에서 읽을 수 있거든요. A의 이력이 없는데도 캐시 읽기가 62,258 - 43,954 = 18,304토큰 더 많았어요. 왕복이 더 들어 같은 앞부분을 여러 번 읽은 것으로 짐작돼요.
두 번째는 맥락이에요. 새 세션은 repository.py가 어디 있는 무슨 파일인지 모르니 파일을 다시 찾고 읽어야 해요. 이어간 쪽은 같은 프로젝트에서 A를 처리한 이력이 있어서 덜 찾고 답할 수 있었을 거예요.
두 이유가 각각 얼마나 기여했는지는 이 한 번의 실행으로 가를 수 없어요. 제 기록에는 두 실행의 num_turns가 남아 있지 않아서, 왕복이 실제로 몇 번 더 들었는지도 확인하지 못했습니다. 직접 재 보실 때는 num_turns를 같이 적어 두세요.
그리고 하나 더 기억하실 게 있어요. 캐시 읽기는 기본 입력 단가의 0.1배예요. 일부 최신 모델은 0.05배나 0.025배로 더 쌉니다. 반대로 캐시 쓰기는 5분짜리 캐시가 1.25배, 1시간짜리 캐시가 2배예요.
토큰 수를 그대로 더하면 이 차이가 안 보이니, 기본 입력 단가를 1로 놓고 환산해 볼게요. 측정 때 모델과 요금 방식은 기록이 없어서, 읽기 0.1배 모델에 5분 캐시(쓰기 1.25배)라고 놓았어요. 출력 토큰도 기록이 없어서 뺐어요.
- 이어서 실행: 4 × 1 + 337 × 1.25 + 43,954 × 0.1 = 약 4,821
- 새로 시작: 6 × 1 + 3,432 × 1.25 + 62,258 × 0.1 = 약 10,522
입력 합으로는 65,696 ÷ 44,295 = 약 1.48배 차이였는데, 단가를 곱하면 10,522 ÷ 4,821 = 약 2.2배가 됩니다. 새로 시작한 쪽은 단가가 비싼 캐시 쓰기가 3,432토큰으로 이어간 쪽 337토큰의 열 배쯤이었기 때문이에요.
이 2.2배는 가장 보수적인 조합의 값이에요. 읽기가 0.05배나 0.025배인 모델이거나 1시간 캐시(쓰기 2배)였다면 같은 식에서 차이가 더 벌어지니, 적어도 약 2.2배로 읽어 주세요. 이력이 커 보여도 대부분이 캐시로 읽히는 한, 유지하는 비용은 생각보다 작아요.
그럼 언제 끊어야 할까요
그러면 "작업마다 새 세션"이라는 조언은 틀린 걸까요? 그건 아니고, 조건이 붙는 거예요.
유지하는 비용은 이력 길이에 비례해서 이후의 모든 요청에 붙어요. 캐시 단가로라도 매번 붙습니다.
그래서 이력이 수십만 토큰까지 가면 이 누적이 커지고, 무엇보다 컨텍스트 창(모델이 한 번에 받을 수 있는 입력의 최대 크기)이 차올라요. 창이 차면 자동 압축이 발동해서 이력이 요약으로 뭉개지고, 그 압축 자체도 비용이에요.
캐시 수명도 조건이에요. 캐시는 기본 5분 동안 읽히지 않으면 사라지고, 읽힐 때마다 수명이 다시 채워져요. Claude Code의 메인 대화는 Claude 구독의 포함 사용량으로 쓰면 1시간, API 키, 사용량 크레딧, 클라우드 공급자로 쓰면 5분이 기본입니다. 수명이 지난 뒤에 이어가면 이력 전체를 캐시에 다시 써야 해서, 위의 결과처럼 이어간 쪽이 싸다고 보기 어려워요.
게다가 무관한 이력이 많을수록 모델이 엉뚱한 맥락을 끌어다 쓰는 오염도 생겨요. 이력 속 비슷한 값을 잘못 집는 일이 어떻게 생기는지 더 궁금하면 에이전트 대화가 길어지면 무엇이 느려지고 무엇을 잊을까? 글을 참고하세요.
그래서 기준은 토큰 수가 아니라 이 두 가지 질문이 되어야 해요.
- 다음 작업이 지금 이력을 참조하나요? 참조한다면 유지하는 편이 유리해요. 위 실측의 B도 A와 다른 파일을 물었지만, 같은 프로젝트를 다룬 이력이 쓸모가 있었던 경우로 볼 수 있어요.
- 이력이 창을 위협할 만큼 길거나 주제가 완전히 바뀌나요? 그러면 끊는 게 맞아요.
같은 세션을 이어 쓰면 비용 말고 판단도 달라지는지, 예를 들어 자기가 고친 코드를 같은 세션에서 검토하면 관대해지는지가 더 궁금하면 Claude가 고친 코드, 같은 세션에서 검토해도 될까? 글을 참고하세요.
화면에서 끊고 압축해봅니다
이번에는 대화형 화면에서 해볼게요. 두 작업이 든 세션을 대화형으로 열면, A와 B가 이력에 들어 있는 상태로 시작해요.
claude --safe-mode --resume "$SID"
화면에서 /context를 치면 컨텍스트가 System prompt, System tools, Messages 같은 항목별로 얼마나 차 있는지 나와요. Messages 항목이 대화가 쌓인 만큼 커져 있을 거예요. 이게 지금 다루고 있는 대화 이력의 몫입니다.
주제를 완전히 바꿀 때는 /clear로 이력을 비웁니다. 다만 그 전에 이력에서 살릴 게 있으면 밖으로 빼 둬야 하는데, 그건 아래에서 다룰게요.
이력이 필요하긴 한데 너무 길기만 할 때는 압축을 씁니다. /compact 뒤에 무엇을 남길지 지시를 붙일 수 있어요.
/compact WIP_LIMIT 관련 결정과 파일 위치는 남겨 줘
압축이 끝나면 /context를 다시 열어서 Messages가 줄었는지 보시면 됩니다.
그런데 화면으로는 줄었다는 것까지만 알 수 있어요. 얼마나 줄었고 시간이 얼마나 걸렸는지는 세션 로그에 남습니다. Claude Code를 종료하고 터미널에서 확인해볼게요.
SESS_LOG="$(find "$HOME/.claude/projects" -type f -name "$SID.jsonl" -print -quit)"
jq -c 'select(.subtype=="compact_boundary") | .compactMetadata | {trigger, preTokens, postTokens, durationMs}' "$SESS_LOG"
첫 줄은 세션 아이디로 그 세션의 로그 파일을 찾아요. 로그는 한 줄에 JSON 하나씩 적힌 jsonl 파일이에요. 둘째 줄은 그 로그에서 압축이 일어난 지점의 기록(compact_boundary)만 골라 네 값을 뽑습니다.
trigger: 직접 친 압축이면manual, 창이 차서 자동으로 걸렸으면autopreTokens,postTokens: 압축 전과 후의 크기durationMs: 압축에 걸린 시간(밀리초)
압축 전후 두 값의 차이가 이번에 요약으로 바뀐 분량이에요. 제 실측 기록에는 압축 전후 값이 없어서, 여기서는 확인하는 방법만 보여드려요.
자동 압축도 같은 원리예요. 창이 차오르면 Claude Code가 알아서 압축하는데, 발동 시점을 조절할 수도 있어요(2026-10 문서 기준).
- 대화 중:
/autocompact <auto|토큰 수> - 실행할 때:
--autocompact플래그 - 설정 파일:
autoCompactWindow - 환경 변수:
CLAUDE_CODE_AUTO_COMPACT_WINDOW(100k~1M)
여기서 중요한 건 압축이 공짜가 아니라는 거예요. 요약을 만드는 것도 결국 모델 호출이고, 요약에서 빠진 세부는 그냥 사라져요.
비용도 언제 압축하느냐에 따라 달라요. 압축하면 이력 부분은 캐시에 다시 써야 하지만, 시스템 프롬프트와 세션 시작 뒤 바뀌지 않은 CLAUDE.md 부분은 그대로 캐시에서 읽혀요. 요약을 만드는 요청도 캐시가 살아 있으면 앞부분을 캐시에서 읽어 싸지만, 오래 쉬었다가 압축하면 전체를 새로 처리해서 가장 비쌉니다.
그래서 압축에 기대기 전에, 살릴 것을 애초에 밖에 남겨 두는 습관이 필요해요.
이력은 휘발되니까 파일로 남깁니다
이력은 결국 휘발돼요. /clear로 지워지고, 압축으로 뭉개지고, 세션이 끝나면 찾기 어려워집니다.
그래서 결정 사항과 진행 상황은 파일로 밖에 남겨요. 파일은 읽히기 전까지 토큰이 들지 않고, 다음 세션에서 필요할 때만 읽히면 되니까요.
예를 들어 프로젝트에 NOTES.md를 이렇게 만들어 둘 수 있어요.
# 작업 노트
## 결정
- WIP_LIMIT은 5로 올리기로 함. 근거: app/service.py:10, 팀 논의
## 진행
- [x] WIP_LIMIT 변경 및 커밋
- [ ] move()의 WIP 우회 경로 수정. 상세: app/service.py:31 근처
형식보다 원칙이 중요해요. 결정에는 근거의 원본 위치를 같이 적으세요. 파일과 줄 번호, 아니면 커밋 해시요.
이렇게 해 두면 나중에 세부가 필요해졌을 때 요약이 아니라 원본으로 돌아갈 수 있어요. 그리고 새 세션에 이 파일 하나만 읽히면 긴 이력 없이도 맥락이 서고요.
이력을 전부 들고 다니는 대신, 원본 위치를 적어 둔 목록만 들고 다니는 거예요.
정리
범위: 작은 Python 프로젝트에서 claude -p --safe-mode로 앞 작업 세션을 이어간 실행과 새 세션 실행을 조건마다 한 번씩 비교한 결과, /compact 기록을 확인하는 방법. 단가 배수와 옵션은 2026-10 문서 기준입니다.
- 이어간 쪽 입력 합은 44,295토큰, 새 세션은 65,696토큰이었어요. 한 번씩만 잰 값이라, "새 세션이 늘 싸다"가 이 경우에는 맞지 않았다는 데까지만 받아들여 주세요.
- 차이는 캐시에서 크게 갈렸어요. 캐시 쓰기가 337 대 3,432였고, 읽기 0.1배와 쓰기 1.25배로 놓고 환산하면 약 4,821 대 10,522로 적어도 약 2.2배였습니다. 비교할 때는
usage세 항목을 보고, 세션 누적인total_cost_usd는 맞대지 마세요. - 끊는 기준은 토큰 수가 아니라 다음 작업이 이력을 참조하는지예요. 이력이 창을 위협하거나 주제가 완전히 바뀌면 끊으세요. 캐시 수명이 지난 뒤에는 이어가도 싸다고 보기 어려워요.
/compact에는 남길 것을 지시할 수 있어요. 얼마나 줄었는지는 세션 로그의compact_boundary기록(preTokens,postTokens)으로 확인하세요.- 끊기 전에 결정과 진행 상황을 원본 위치(파일과 줄 번호, 커밋 해시)와 함께 파일로 남기세요.
이 시리즈의 다음 글에서는 이 캐시가 언제 쌓이고, 무엇이 깨뜨리고, 깨진 걸 어떻게 알아채는지 다뤄요.