
결론부터 말씀드리면, 토큰 사용량 집계 대시보드는 총 토큰 수 하나만 보여줘서는 쓸모가 없습니다. 모델별, 입력·출력 방향별, 캐시 히트 여부별로 쪼개서 쌓아야 비용 분석이 가능해지고 이상 징후도 잡아낼 수 있습니다. 이 글에서는 어떤 필드를 어떤 단위로 모아야 하는지, 그리고 실시간 집계가 오히려 방해가 되는 상황은 언제인지까지 구체적으로 짚어보겠습니다.
토큰 사용량 집계, 범위를 어디까지 잡아야 할까요
가장 흔한 실수는 API 응답의 total_tokens 값 하나만 저장하는 것입니다. 이 값만으로는 요금이 왜 올랐는지 원인을 찾을 수 없습니다. 대부분의 LLM API는 입력 토큰과 출력 토큰을 분리해서 반환하고, 많은 모델에서 출력 토큰 단가가 입력 토큰보다 높게 책정되어 있어서 이 둘을 나눠 쌓지 않으면 비용 분석 자체가 성립하지 않습니다.
여기에 더해 프롬프트 캐싱을 쓰는 서비스라면 캐시 생성 토큰과 캐시 읽기 토큰도 별도 필드로 필요합니다. Anthropic Messages API는 usage 객체에 input_tokens, output_tokens와 함께 cache_creation_input_tokens, cache_read_input_tokens를 함께 내려줍니다. 이 네 값을 그대로 저장하는 것이 최소 단위 집계의 출발점입니다.
모델별·엔드포인트별로 쪼개야 비용 분석이 의미를 가집니다
같은 서비스 안에서도 요약용으로는 저렴한 모델을, 복잡한 추론에는 비싼 모델을 섞어 쓰는 경우가 많습니다. 이때 모델 구분 없이 합산하면 어느 기능이 비용을 끌어올리는지 알 수 없습니다. 집계 키를 (모델명, 엔드포인트, API 키 또는 사용자 ID, 날짜) 조합으로 잡아야 특정 기능·특정 고객의 비용 추이를 따로 뽑아볼 수 있습니다.
| 집계 단위 | 필요한 시점 | 저장 비용 |
|---|---|---|
| 전체 합계만 | 예산 초과 여부만 확인할 때 | 매우 낮음 |
| 모델별 | 모델 교체·요금제 변경을 검토할 때 | 낮음 |
| 사용자·API 키별 | 특정 고객·팀 과금을 나눠야 할 때 | 중간 |
| 요청 단위 원본 로그 | 이상 요청 추적, 사후 재계산이 필요할 때 | 높음 |

이렇게 태그를 나눠 비용을 추적하는 방식은 클라우드 비용 관리 분야에서 말하는 핀옵스 방법론의 원리와 크게 다르지 않습니다. 인프라 비용을 팀·프로젝트 태그로 나눠 책임 소재를 명확히 하듯, 토큰 비용도 같은 방식으로 나눠야 누가 얼마를 쓰는지 설명할 수 있습니다.
캐시 히트 토큰을 놓치면 생기는 오차
프롬프트 캐싱이 켜진 서비스에서 캐시 읽기 토큰을 일반 입력 토큰과 같은 단가로 취급하면 예상 비용이 실제보다 크게 부풀려집니다. 반대로 캐시 쓰기 토큰을 빼먹으면 실제 청구액보다 낮게 잡히는 경우도 생깁니다. 정확한 단가 배율은 시점마다 바뀔 수 있어서 이 부분은 Anthropic 공식 API 문서의 usage 필드 설명과 요금 페이지를 직접 확인하는 편이 안전합니다.
토큰이 잘게 나뉘어 계산되는 과정 자체는 자연어처리에서 텍스트를 의미 단위로 쪼개는 방식에서 온 것이라, 모델마다 같은 문장도 토큰 수가 달라질 수 있습니다. 그래서 “글자 수 대비 토큰 수”를 고정 비율로 가정하고 대시보드를 설계하면 모델을 바꾸는 순간 집계가 어긋납니다.
대시보드에 이 필드부터 추가해 보세요
가장 먼저 손댈 부분은 요청 단위 원본 로그입니다. API 응답을 받는 지점에서 아래처럼 최소 필드를 그대로 저장해 두면, 나중에 어떤 방식으로든 재집계가 가능합니다.
import time
import json
def record_usage(response, model, api_key_id):
usage = response.usage
row = {
"ts": int(time.time()),
"model": model,
"api_key_id": api_key_id,
"input_tokens": usage.input_tokens,
"output_tokens": usage.output_tokens,
"cache_creation_tokens": getattr(usage, "cache_creation_input_tokens", 0),
"cache_read_tokens": getattr(usage, "cache_read_input_tokens", 0),
}
with open("usage_log.jsonl", "a") as f:
f.write(json.dumps(row) + "\n")
return row
# 예시 호출 결과
sample = record_usage(response, "claude-3-5-sonnet", "key_abc123")
print(sample)
이 스크립트를 실행하면 다음과 같은 한 줄이 로그 파일에 쌓입니다.

{"ts": 1757043000, "model": "claude-3-5-sonnet", "api_key_id": "key_abc123", "input_tokens": 812, "output_tokens": 340, "cache_creation_tokens": 0, "cache_read_tokens": 780}
이렇게 원본을 남겨 두면 나중에 요금 정책이 바뀌어도 저장된 토큰 수를 기준으로 다시 계산할 수 있습니다. 반대로 처음부터 “예상 비용(원)”만 계산해서 저장하면, 단가가 바뀌는 순간 과거 데이터를 다시 못 믿게 됩니다.
실시간 집계가 항상 정답은 아닌 이유
스트리밍 응답을 쓰는 서비스라면 usage 필드가 스트림의 마지막 이벤트에서만 내려오는 경우가 많습니다. 클라이언트가 응답 중간에 연결을 끊으면 최종 토큰 수를 못 받는 요청이 생기고, 이 요청들을 어떻게 처리할지 규칙을 정해두지 않으면 집계가 실제보다 낮게 잡힙니다. 끊긴 요청은 추정치로 채우거나 별도 “미완료” 상태로 표시해 총합에서 빼는 방식으로 구분하는 편이 낫습니다.
또한 요청마다 실시간으로 대시보드를 갱신하는 구조는 트래픽이 많지 않은 서비스에서는 유지 비용 대비 얻는 게 적습니다. 하루 요청이 수백 건 수준이라면 5분이나 1시간 단위 배치 집계로도 충분하고, 실시간 파이프라인을 따로 구축하는 엔지니어링 비용이 오히려 더 클 수 있습니다. 요청량이 초당 수십 건을 넘어가는 시점부터 실시간 집계의 이득이 커진다고 보는 편이 현실적입니다.
결국 임계값 알림 하나로 이어지는 토큰 사용량 집계
정리하면 대시보드에 필요한 최소 필드는 모델명, 입력·출력 토큰, 캐시 생성·읽기 토큰, 그리고 사용자나 API 키 식별자입니다. 이 다섯 가지만 요청 단위로 저장해도 모델별 비용 분석, 특정 사용자의 이상 사용 탐지, 캐싱 도입 효과 검증까지 대부분 가능해집니다. 반대로 실시간성이나 세분화를 과하게 추구하면 트래픽이 적은 서비스에서는 유지 비용만 늘어난다는 점도 함께 고려해야 합니다.
오늘 바로 해볼 수 있는 작업은 두 가지입니다. 기존 로그에 cache_read_tokens, cache_creation_tokens 필드가 빠져 있다면 지금 추가하고, 일일 예산 초과 시 알림을 보내는 임계값 하나만 먼저 걸어두는 것입니다. 나머지 세분화는 데이터가 쌓인 뒤 필요한 만큼 늘려가도 늦지 않습니다.
