Spring AI 토큰 사용량: 모델을 선택하기 전 비용 측정하기 — LLM 비용 제어 1/4
요약
Spring AI를 사용하여 LLM의 토큰 사용량을 측정하고 비용을 제어하는 방법을 다룹니다. Micrometer 기반의 관측 가능성을 활용해 모델별, 토큰 유형별 지출을 애플리케이션 내부 수준에서 추적하는 기초를 설명합니다.
핵심 포인트
- LLM 비용 절감을 위해 토큰 사용량에 대한 정밀한 측정 필요
- Spring AI는 Micrometer를 통해 모델 및 실행 관찰 데이터 제공
- ChatModel, EmbeddingModel 등을 통한 토큰 사용량 지표 확인 가능
- 애플리케이션 내부 기능별 비용 발생 원인 파악의 중요성
Spring AI에서 LLM 비용을 절감하는 것은 두 가지 선택에서 시작됩니다: 어떤 모델이 요청에 응답할 것인가, 그리고 ChatClient가 보낼 때마다 모든 요청에 어떤 기본값(defaults)을 추가할 것인가입니다.
토큰이 어디로 가는지 확인할 수 있기 전까지는 그 어느 것도 변경할 가치가 없습니다. 이것이 이 글이 측정(measurement)부터 시작하는 이유입니다.
이 글은 4부작 중 제1부이며, 10가지 비용 동인(cost drivers) 중 처음 세 가지를 다룹니다. 동인 #0은 돈이 실제로 어디로 가는지 알려주며, #1과 #2는 애플리케이션이 보내는 모든 요청을 형성하는 두 가지 결정 사항입니다. 나머지 7개는 여기서 구축하는 내용에 연결됩니다.
숫자에 관한 참고 사항: 논의 과정에서 가격 비율이 중요한 경우(입력 vs 출력, 캐시 읽기 vs 쓰기), 이 시리즈는 실제 2026년 7월 리스트 가격을 링크와 함께 인용합니다. 다른 모든 예시는 입력 토큰 100만 개당 $1의 고정 요율을 사용하므로, 귀하의 제공업체 가격표를 사용하여 계산을 다시 수행할 수 있습니다. 이러한 가격은 몇 달마다 변경되므로 직접 확인해 보아야 합니다.
동인 #0 — Spring AI 관측 가능성(observability) 측정: 볼 수 없는 것은 줄일 수 없다
제공업체의 인보이스(invoices)와 사용량 대시보드는 보통 모델별, 토큰 유형별(입력, 출력, 캐시됨) 지출을 보여줍니다. 이는 유용하지만 충분하지는 않습니다. 숫자는 애플리케이션 내부의 어떤 기능(feature), 클라이언트(client), 또는 어드바이저(advisor)가 해당 사용량을 발생시켰는지 알려줄 수 없기 때문입니다.
Spring AI는 이러한 공백을 메우기 위해 Spring Boot의 Micrometer 기반 관찰 가능성 (observability)과 통합됩니다. 핵심 AI 컴포넌트들이 해당 데이터를 자동으로 방출합니다. ChatModel, EmbeddingModel, 그리고 ImageModel 구현체들은 (제공업체마다 지원 여부가 다름) 사용 가능한 경우 토큰 사용량을 포함한 모델 수준의 관찰 데이터 (observations)를 게시합니다. ChatClient (어드바이저 포함) 및 VectorStore는 토큰 사용량 지표 대신 실행 관찰 데이터 (execution observations) 및 추적 (traces)을 보고합니다.
각 지표(metric)에는 모델 이름 및 토큰 유형(token type)과 같은 내장 태그(built-in tags)가 포함되어 있습니다. 이러한 태그는 모델과 제공자(provider)를 구분하지만, 호출자(caller)를 구분하지는 않습니다. 즉, 동일한 모델에 대한 모든 요청은 동일한 태그 값을 가지므로, 태그 자체만으로는 두 기능을 구분할 수 없습니다. Spring AI는 태그를 저카디널리티(low-cardinality) 또는 고카디널리티(high-cardinality)로 표시합니다. 저카디널리티 태그는 지표(metrics)와 추적(traces) 모두에 포함되며, 고카디널리티 태그는 추적(traces)에만 포함됩니다. 특정 기능 또는 클라이언트에 사용량을 할당하려면, 자체적인 저카디널리티 태그(https://javadoc.io/doc/io.micrometer/micrometer-observation/latest/io/micrometer/observation/ObservationConvention.html)를 추가하세요. 이러한 값은 사용자나 요청당 하나씩 생성하지 말고, 적은 수를 유지하며 안정적으로 관리해야 합니다. 태그의 작동 방식과 태그 수를 제어하는 방법에 대한 자세한 내용은 Micrometer의 태그 및 명명 규칙 문서를 참조하세요.
이 데이터가 준비되면, 이후 드라이버에서 제기할 질문들에 답할 수 있습니다. 히스토리 증가가 (Part 2) 실제로 비용을 증가시키고 있는가? 프롬프트 토큰(prompt token) 사용량을 시간에 따라 관찰하십시오. 도구 검색 어드바이저(tool-search advisor) _(Part 3)_가 모델로 전송되는 컨텍스트(context) 양을 줄였는가? 이를 활성화하기 전과 후의 프롬프트 토큰 사용량을 비교하십시오. 추론 모델(reasoning model)이 예상보다 더 비싸지고 있는가 (Part 2)? 출력 토큰(output token) 트렌드를 통해 예상치 못하게 긴 응답과 상승하는 생성 비용을 확인할 수 있습니다.
두 가지 단계를 더 거치면 이 설정은 단순히 정보를 제공하는 수준을 넘어 더 안전해집니다. 첫째, 토큰 사용량이나 요청 볼륨(request volume)의 갑작스러운 증가에 대한 알림(alert)을 생성하세요. LLM 호출을 루프(loop) 안에 넣는 버그는 예산을 빠르게 소진할 수 있으며, 알림을 설정하면 월말이 아닌 몇 분 내에 이를 포착할 수 있습니다. 둘째, 예산(budgets), 할당량(quotas), 또는 사용 제한(usage limits)과 같이 사용 가능한 경우 제공자 측(provider-side)의 지출 제어 기능을 구성하세요. 이는 어떤 애플리케이션 프레임워크도 강제할 수 없는 최종적인 안전장치입니다.
관측성 (Observability)을 0단계로 취급하세요. 다른 무엇을 최적화하기 전에 관측성을 먼저 설정해야, 이후의 모든 변경 사항에 대해 측정 가능한 전후 비교가 가능해집니다.
동인 #1 — 모델 선택: 미니 모델 작업에 플래그십 가격을 지불하는 것을 멈추세요
LLM 애플리케이션에서 비용을 초과 지출하는 가장 빠른 방법은 찾을 수 있는 가장 유능한 모델로 모든 요청을 보내는 것입니다. 동일한 제공자의 가격표에서 플래그십(flagship) 모델과 저가형(budget) 모델은 토큰당 가격이 10배 이상 차이 날 수 있습니다. 하지만 분류(classification), 추출(extraction), 라우팅(routing), 짧은 요약(short summaries)과 같은 일반적인 워크로드의 상당 부분은 이미 더 작은 모델에서도 품질 기준을 통과합니다. 이러한 요청을 플래그십 모델로 보내는 것은 결과물을 개선하지 않으면서 비용만 증가시킵니다.
하지만 가격은 하나의 요소일 뿐입니다. 모델은 수행할 수 있는 기능에서도 차이가 납니다. 어떤 모델은 이미지나 오디오를 수용하고, 어떤 모델은 도구 호출(tool calling)이나 엄격한 구조화된 출력(structured output)을 위해 구축되었으며, 어떤 모델은 단계별로 추론(reasoning)하는 반면 다른 모델은 단순히 직접 답변합니다. 어떤 경우든 규칙은 동일합니다. 작업이 실제로 필요로 하는 기능을 충족하는 가장 작은 모델을 선택하세요. 텍스트 전용 파이프라인(pipeline)에 이미지 지원 비용을 지불하지 말고, 단순히 JSON 형식을 재구성하기 위해 추론 모델(reasoning model)에 비용을 지불하지 마세요. 요구 사항은 시간이 지남에 따라 변하므로, 모델 선택을 최종적인 것이 아닌 일시적인 것으로 취급하세요. 제공자들은 몇 달마다 더 저렴하고 더 나은 모델을 출시하며, 어제 플래그십 모델이 필요했던 작업이 다음 분기에는 미니 모델에서도 충분히 잘 작동하는 경우가 많습니다.
이러한 이점은 모델 전환이 쉬울 때만 실질적인 효과를 발휘합니다. Spring AI의 해답은 모델이 아키텍처(architecture)가 아닌 설정(configuration)이라는 점입니다. ChatClient는 특정 제공자(provider)나 모델에 의존하지 않습니다. 즉, 서비스 코드는 어떤 모델이 응답하는지 알 필요가 없으며, maxTokens나 temperature와 같은 이식 가능한(portable) 옵션들은 모델 간에 그대로 유지되고, 제공자별 설정은 하나의 옵션 객체 내에 격리되어 유지됩니다. Spring AI 2.0은 이를 명시적으로 구현합니다. ChatClient에 설정된 기본 옵션은 모델의 초기 설정에 대한 부분적인 "델타(delta)" 역할을 합니다. 프레임워크는 자체 기본값에서도 동일한 개념을 보여줍니다. 2.0 버전의 OpenAI 통합은 플래그십 모델이 아닌 gpt-5-mini를 기본값으로 사용합니다. 비용 계층(cost tier)별로 클라이언트를 하나씩 두는 것이 좋은 패턴입니다.
@Configuration
public class ChatClients {
...
티켓 분류(ticket classifier) 기능은 @Qualifier("cheapClient") ChatClient를 주입받고, 계약 분석(contract-analysis) 기능은 플래그십 모델을 주입받을 수 있습니다. 여기서 두 가지 세부 사항이 중요합니다. 첫째, 가공되지 않은 ChatClient.builder(chatModel)가 아니라 자동 설정된 빌더(auto-configured builder)를 통해 클라이언트를 구축하십시오. 자동 설정된 빌더에는 이미 관찰 가능성(observability)이 연결되어 있으며, 드라이버 #0의 토큰 메트릭(token metrics)은 이에 의존하기 때문입니다. 둘째, 이 예제는 하나의 제공자 내에서 두 개의 계층을 다룹니다. 만약 동일한 애플리케이션 내에서 여러 벤더(vendor)를 혼합하여 사용한다면, 각 자동 설정된 ChatModel로부터 별도의 ChatClient를 생성하십시오. 어떤 방식이든 변경 사항의 규모는 작게 유지됩니다. 모델을 교체하는 것은 속성(property) 변경일 뿐이며, 벤더를 교체하는 것은 스타터 의존성(starter dependency)과 속성을 추가하는 것뿐입니다. 코드 변경도, 프롬프트 재작성도, 새로 배워야 할 SDK도 필요 없습니다.
드라이버 #2 — 하나의 공유 클라이언트: 모든 요청이 모든 기본값을 전달함
ChatClient의 기본 설정은 공짜가 아닙니다. 클라이언트에 연결된 모든 것 — 시스템 프롬프트 (system prompt), 도구 (tools), 메모리 어드바이저 (memory advisor) — 은 해당 클라이언트가 보내는 모든 요청에 포함됩니다. 하나의 공유된 "모든 것을 수행하는" 클라이언트를 사용한다는 것은, 단순한 10토큰 호출(예: 분류기 기능)이 가장 복잡한 기능과 동일한 2,000토큰의 시스템 프롬프트, 도구 스키마 (tool schemas), 그리고 대화 기록 (conversation history)을 함께 운반한다는 것을 의미합니다. 만약 분류기가 한 달에 50,000번 실행된다면, 이는 실제로 전혀 필요하지 않은 추가 콘텐츠로 인해 1억 개의 입력 토큰이 발생하는 것이며, 예시 요율 기준으로 아무런 이득 없이 매달 100달러를 지불하는 셈입니다.
Spring AI의 비용 제어 단위는 애플리케이션 전체가 아니라 클라이언트입니다. ChatClient.Builder는 프로토타입 빈 (prototype bean)으로 자동 구성되므로, 각 작업은 해당 작업에 필요한 기본값만을 사용하여 자신만의 클라이언트를 구축할 수 있습니다: 프롬프트를 위한 .defaultSystem(), 모델 및 토큰 설정을 위한 .defaultOptions(), 그리고 나머지를 위한 .defaultAdvisors() 및 .defaultTools()가 그것입니다. 이 시리즈의 이후 모든 드라이버 — 메모리 윈도우 (memory windows), RAG 어드바이저 (RAG advisors), 도구 검색 (tool search) — 는 정확히 이 단계에서 연결됩니다. 이것이 작업별 클라이언트를 구축하는 것이 다음에 이어질 드라이버들보다 선행되어야 하는 이유입니다.
@Configuration
class TaskClients {
...
Spring AI 2.0 변경 사항에 유의하세요:
.defaultOptions()와 호출 시 사용하는.options()는 이제 완전히 빌드된 인스턴스가 아니라ChatOptions.Builder를 받습니다. 이 빌더는 첫 번째 어드바이저가 실행되기 전에 모델의 기본값과 병합되며, 명시적으로 설정한 필드만 기본값을 덮어씁니다. 이러한 병합을 통해 모델에 구성된 기본값을 교체하지 않고도 필요한 옵션만 덮어쓸 수 있습니다:
classifierClient.prompt(text)
.options(ChatOptions.builder().maxTokens(1)) // yes/no 케이스를 위한 덮어쓰기
.call()
...
결과적으로: 각 요청은 해당 작업이 실제로 선언한 기본값에 대해서만 비용을 지불합니다. 이는 이후에 이어지는 모든 제어 방식의 필수 요구 사항입니다.
다음 단계
드라이버(Driver) #1과 #2는 어떤 모델이 요청에 응답할지, 그리고 어떤 클라이언트(Client)가 요청을 보낼지를 결정합니다. 드라이버(Driver) #0은 이러한 결정에 비용이 얼마나 드는지 보여줍니다. 이제 여러분은 작업당 하나의 클라이언트를 가지게 되었으며, 각 클라이언트는 해당 작업에 필요한 기본값(Defaults)만을 포함하고 이를 증명할 토큰 메트릭(Token metrics)을 갖추고 있습니다.
이 중 그 어떤 것도 단일 요청의 크기를 제한하지는 않습니다. 추론 모델(Reasoning model)은 눈에 보이는 답변보다 더 많은 숨겨진 사고(Hidden thinking) 과정에 대해 비용을 청구할 수 있습니다. 고객 지원 대화(Support conversation)는 매 턴마다 전체 대화 기록을 다시 전송합니다. 동일한 시스템 프롬프트(System prompt)는 제공자(Provider)가 캐싱(Caching)할 수 있도록 구조화하지 않는 한, 매일 수천 번씩 전체 비용을 지불하며 전송됩니다.
파트 2(Part 2)에서는 이 세 가지를 다룹니다. 각각은 여러분이 방금 구축한 클라이언트(Clients)에 부착하는 제한 사항(Limit) 또는 조언자(Advisor) 역할을 합니다.
파트 2(Part 2)는 2026년 8월에 공개됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기