본문 바로가기
카테고리 없음

LLM API 비용의 토큰 낭비 (패턴 진단, 히스토리 관리, 캐싱 전략)

by BOOST YOUR INFORMATION 2026. 5. 28.

LLM 토큰 낭비 (패턴 진단, 히스토리 관리, 캐싱 전략) 참조 이미지
LLM 토큰 낭비 (패턴 진단, 히스토리 관리, 캐싱 전략)

 

파트너사의 고객 지원 봇을 처음 진단했을 때 솔직히 예상 밖이었다. 월 API 비용의 68%가 실제 AI 처리가 아닌 히스토리 재전송에 쓰이고 있었거든요. 코드는 멀쩡히 돌아가고 있었고, 아무도 문제를 몰랐다. LLM 개발에서 토큰 낭비는 대부분 이렇게 조용히 쌓인다. 이 숫자를 처음 봤을 때 든 생각은 "버그가 있는 거 아닌가"였다. 그런데 코드를 뜯어보면 뜯어볼수록 버그가 아니라 설계였다. 아무도 의심하지 않았기 때문에 아무도 고치지 않은 패턴들이 비용 구조를 조용히 갉아먹고 있었다. 이 경험 이후로 "돌아가는 코드"와 "효율적인 코드"가 얼마나 다를 수 있는지를 다시 생각하게 됐다.

가장 많이 보이는 낭비 패턴 다섯 가지

직접 코드 리뷰와 프로덕션 모니터링을 하면서 반복적으로 마주친 패턴들이다.

첫 번째: 역할 지정 과잉 (Role Bloat)

Role Bloat이란 시스템 프롬프트에서 AI의 페르소나를 지나치게 길고 상세하게 설정하는 현상이다. "당신은 10년 경력의 시니어 소프트웨어 엔지니어이자 아키텍트이며 DevOps 전문가로서, 객체지향 설계와 함수형 프로그래밍 모두에 능통하고..." 같은 식이다. 직접 측정해보니 이런 역할 지시만으로 150~300 토큰이 추가로 소비되고 있었는데, "당신은 숙련된 소프트웨어 엔지니어입니다." 한 줄과 실제 출력 품질 차이는 거의 없었다.

솔직히 이 패턴은 개발자들이 의도적으로 만드는 낭비다. "더 자세히 설명하면 더 좋은 결과가 나올 것"이라는 직관이 있는데, 나도 처음에는 그렇게 생각했다. 그런데 실험해보면 현대 LLM은 역할 설명 두 줄과 스무 줄의 차이를 생각보다 훨씬 적게 느낀다. 정작 중요한 건 역할 묘사의 길이가 아니라 실제 작업 지시의 명확성이었다. 역할을 길게 쓰는 데 들인 공을 작업 지시를 명확히 하는 데 썼다면 더 나은 결과를 얻었을 것이다.

두 번째: Few-shot 예시 과다 삽입

Few-shot이란 모델에게 작업 방식을 예시로 보여주는 기법이다. 많은 팀이 예시를 5개 이상 넣는 것을 당연하게 여긴다. 실제로 써봤는데, 잘 선택된 예시 1~2개가 5개짜리 예시 묶음과 거의 동일한 품질을 냈다. 예시가 많을수록 좋다는 생각은, 적어도 입력 토큰 비용 측면에서는 틀렸다.

여기서 한 발 더 나아가서 생각한다. 예시의 수보다 예시의 질이 훨씬 중요하다. 애매한 예시 다섯 개보다 엣지 케이스를 정확히 보여주는 예시 한 개가 훨씬 효과적이었다. 팀에서 Few-shot 예시를 고를 때 충분한 시간을 들이지 않고 "일단 많이 넣으면 낫겠지"라는 생각으로 접근하는 경우가 많은데, 이게 이중으로 나쁘다. 비용도 높이고 품질도 그만큼 향상되지 않으니까.

세 번째: 장황한 툴 정의 파라미터 설명

툴 정의(function calling)란 LLM이 외부 함수를 호출할 수 있도록 함수의 이름과 파라미터를 명세하는 방식이다. 이 명세가 매 API 호출마다 입력 토큰에 포함되는데, 불필요하게 길고 장황한 파라미터 설명은 모델 성능에 거의 영향을 주지 않으면서 토큰만 낭비한다. 이 패턴이 특히 고약한 이유는 축적 효과 때문이다. 툴을 10개 정의해두고 각 파라미터 설명이 과하게 길다면, 그 비용이 매 단일 호출마다 고스란히 붙는다. 하루 1만 건 호출이라면 낭비 토큰이 수백만 개 단위로 쌓인다. 파라미터 설명은 모델이 파라미터의 의도를 파악할 수 있을 만큼만 작성하면 충분하다. 완벽한 문서화를 프롬프트에서 구현하려는 욕심은 비용 낭비다. 나는 툴 정의를 처음 작성할 때 개발 문서를 쓰듯이 길게 썼다가, 그게 매 호출마다 청구서에 찍힌다는 걸 뒤늦게 깨달았다.

네 번째: 구조화된 출력 형식 미사용

내부 파이프라인에서 출력을 프로그래밍으로 파싱할 것임에도 "서론, 본론, 결론 구조로 나누어 각 섹션에 소제목을 달고, 핵심 포인트는 불릿으로 정리하되..." 같은 서술형 지시를 붙이는 경우가 많다. 이런 형식 지시가 출력 토큰의 40~60%를 차지하는 케이스를 실제로 본 적이 있다. "JSON 형식으로만 출력, 설명 없음" 한 줄이 훨씬 효율적이다.

이건 내가 개인적으로 가장 빠르게 고칠 수 있는 낭비라고 생각한다. 파이프라인 끝에 파서가 붙어있다면 출력 형식을 최대한 압축하는 게 맞다. 사람이 읽을 필요가 없는데 사람이 읽기 좋은 형식으로 출력하도록 요청하는 것은, 프린터기에 잉크를 낭비하는 것과 같다. 용도에 맞는 출력 형식을 명확히 지정하는 습관이 중요하다.

다섯 번째: 타임스탬프 시스템 프롬프트 삽입

캐싱을 설정해 놓고 시스템 프롬프트에 현재 타임스탬프를 동적으로 삽입하는 경우다. 타임스탬프가 매 초 바뀌니 캐시 히트율이 0%가 된다. 이 패턴은 특히 캐싱 도입 후에도 비용이 줄지 않는다는 피드백을 받았을 때 가장 먼저 확인하는 항목이 됐다.

이 실수는 캐싱의 동작 원리를 이해하지 못한 채 캐싱 기능을 켜두기만 한 경우에 발생한다. 캐싱은 프롬프트가 동일할 때만 작동한다. 시스템 프롬프트에 동적 값이 하나라도 들어가는 순간 캐싱은 무력화된다. 이 문제를 발견한 뒤로 캐싱 관련 디버깅의 첫 번째 체크리스트 항목으로 "시스템 프롬프트에 동적 값이 있는가"를 올려뒀다. 타임스탬프가 들어간 이유를 물어보면 대부분 "디버깅 때 편하려고요"라는 대답이 돌아온다. 디버깅 목적이라면 별도 로그에 남기는 게 맞다.

히스토리 관리, 왜 비용 폭탄이 되는가

멀티턴 대화의 비용이 왜 기하급수적으로 오르는지 많은 사람들이 직관적으로 이해하지 못한다. Claude를 포함한 대부분의 LLM은 스테이트리스(stateless) 모델이다. 스테이트리스란 모델 자체가 이전 대화를 기억하지 않는다는 뜻으로, 매 API 호출마다 전체 대화 이력을 통째로 재전송해야 한다. 서버가 아니라 클라이언트가 상태를 들고 다니는 구조다.

이 구조를 이해하지 못한 채 무한정 히스토리를 쌓으면 어떻게 될까. 내가 진단한 고객 지원 봇이 딱 그 케이스였다. 50턴 대화 기준으로 마지막 메시지의 입력 토큰이 첫 번째 메시지의 50배에 달했고, 월 비용의 68%가 히스토리 재전송 비용이었다. 실제로 필요한 최근 컨텍스트는 500~1,000 토큰이면 충분한 경우가 대부분인데, 20턴 대화가 5,000~10,000 토큰을 소비하고 있었던 것이다.

히스토리 관리 전략에 대해서는 의견이 나뉜다. "히스토리를 요약해서 압축하면 충분하다"는 시각도 있고, "슬라이딩 윈도우 방식으로 최근 N턴만 유지하는 게 더 단순하고 효과적"이라는 시각도 있다. 나는 후자 쪽이었는데, 직접 운영하면서 생각이 바뀌었다. 슬라이딩 윈도우는 단순하고 비용 절감 효과도 즉각적이지만, 초반 대화의 핵심 맥락이 창 밖으로 밀려나는 순간 서비스 품질이 눈에 띄게 떨어진다. 요약이 추가 API 호출을 발생시키는 단점이 있어도, 고객 지원처럼 초반 정보가 대화 내내 중요한 서비스라면 요약 기반 접근이 현실적으로 더 낫다. 태스크 성격을 먼저 파악하고 전략을 선택하는 것이 중요하다. 모든 서비스에 통하는 단일 전략은 없다.

캐싱 전략, 단일 최적화 중 ROI가 가장 높다

캐싱 이야기를 꺼내면 "어차피 LLM API가 알아서 해주는 거 아닌가요?"라고 묻는 사람들이 종종 있는데, 그렇지 않다. 프롬프트 캐싱(Prompt Caching)이란 동일한 시스템 프롬프트가 반복 전송될 때 첫 호출에서만 전체 처리 비용을 내고, 이후 호출에서는 캐시된 버전을 재사용해 비용을 대폭 낮추는 기능이다. Anthropic Claude API 기준으로 캐시 쓰기는 표준 입력 토큰 비용의 1.25배, 캐시 읽기는 0.1배가 적용된다.

계산해보면 효과가 분명해진다. 5,000 토큰짜리 시스템 프롬프트를 하루 1,000번 전송한다고 가정할 때, 캐싱 없이는 하루 500만 토큰의 입력 비용이 발생한다. 캐싱 적용 후에는 첫 호출만 1.25배 비용을 내고 나머지 999번은 0.1배로 처리되니, 이론적으로 하루 비용이 약 90% 감소한다. 직접 적용해본 사례에서도 단일 최적화로는 가장 ROI가 높았다.

단, 앞서 언급한 타임스탬프 삽입 문제처럼 캐시를 무력화하는 패턴에는 항상 주의해야 한다. 동적으로 바뀌는 값이 시스템 프롬프트 안에 있으면 캐시 키가 매번 달라지고 캐시 히트가 발생하지 않는다. 해결책은 간단하다. 날짜나 사용자 정보처럼 동적으로 변하는 요소는 시스템 프롬프트 밖으로 분리해서 사용자 메시지나 별도 컨텍스트 블록에 담으면 된다. 실무에서 캐싱 도입 후 비용이 줄지 않는다는 피드백을 받은 케이스의 상당수가 이 패턴 때문이었다.

캐싱을 처음 도입할 때 나도 "선언만 하면 끝"이라고 생각했다. 그런데 캐시 히트율을 모니터링하기 시작하니 생각보다 히트율이 낮은 경우가 꽤 있었고, 원인을 추적하다 보면 대부분 동적 값이 섞여 있는 게 문제였다. 캐싱 도입과 함께 usage 필드의 cache_read_input_tokens 값을 로깅하는 것을 반드시 같이 해두는 것을 권장한다. 숫자가 보여야 뭘 고쳐야 할지 보인다. 히트율이 낮다는 것 자체가 어딘가에 동적 값이 들어가 있다는 신호인 경우가 많다.

결국은 문화의 문제다

결국 토큰 낭비 문제는 기술적 무지의 문제가 아닌 경우가 많다. "일단 동작하게 만들자"는 초기 개발 문화가 그대로 굳어버리는 것이 근본 원인이다. 제품이 성장하면 비용도 같이 성장하는데, 최적화 없이 쌓인 기술 부채는 나중에 리팩터링하기가 훨씬 어렵다.

현실적인 출발점은 거창한 리팩터링이 아니다. PR 리뷰 체크리스트에 "이 프롬프트의 토큰 수는 측정되었는가?" 한 줄을 추가하는 것으로 시작할 수 있다. 토큰 수를 측정하는 습관이 생기면 자연스럽게 낭비 패턴이 눈에 들어온다. 작은 문화적 변화가 비용 구조를 바꾼다. 나도 이 작업을 팀 루틴에 넣기 전과 후로 비용 감각이 완전히 달라졌다. 측정하지 않으면 개선할 수 없다. 이건 LLM 비용에만 적용되는 이야기가 아니라, 모든 엔지니어링 최적화에 해당하는 원칙이다. 다만 LLM에서 이 원칙이 유독 잘 무시되는 건, 결과가 토큰 단위로 청구서에 쌓이기 전까지는 눈에 보이지 않기 때문이다.


참고


소개 및 문의 · 개인정보처리방침 · 면책조항

© 2026 ⚡ 정보 부스터 🚀