LLM API를 서버에서 호출하는 건 일반 REST API 호출과 다릅니다. 토큰 과금, Context Window 한계, Rate Limit, Streaming을 이해해야 제대로 된 AI 서비스를 만들 수 있습니다.
LLM은 텍스트를 문자나 단어 단위로 처리하지 않고 토큰(Token) 단위로 처리합니다. 토큰은 단어보다 작은 단위로, 영어는 평균 1토큰 ≈ 0.75단어, 한국어는 1토큰 ≈ 0.3~0.5글자입니다.
Context Window는 LLM이 한 번의 요청에서 처리할 수 있는 최대 토큰 수입니다.
입력(프롬프트 + 대화 이력)과 출력(응답)을 합친 총량이 이 한도를 초과할 수 없습니다.
Claude 3.7 Sonnet: 200K 토큰 (약 책 150페이지 분량)
GPT-4o: 128K 토큰
Context가 꽉 차면 이전 대화를 잘라내거나 요약해야 합니다. 이 전략을 Context Management라 합니다.
| 모델 | 입력 토큰 (1M당) | 출력 토큰 (1M당) | 특이사항 |
|---|---|---|---|
| Claude Sonnet 4.5 | $3.00 | $15.00 | Prompt Caching 지원 (90% 할인) |
| GPT-4o | $2.50 | $10.00 | Vision 포함 |
| GPT-4o mini | $0.15 | $0.60 | 저비용 라우팅용 |
| Gemini 1.5 Pro | $1.25 | $5.00 | 2M Context |
// 잘못된 프롬프트 설계
String prompt = "이 코드를 리뷰해줘. 가능한 한 상세하게 설명해줘.";
// → 모델이 장황하게 출력 → 비용 폭발
// 출력 토큰 제한
request.setMaxTokens(500); // 필수!
// 구조화된 출력 요구
String prompt = "다음 JSON 형식으로만 답해줘: " +
"{\"issues\": [...], \"score\": 0-10}";
// 긴 시스템 프롬프트가 매 요청마다 반복되면 낭비
// Anthropic Prompt Caching: 동일 prefix는 캐시
// Java SDK 예시 (Anthropic)
Message msg = client.messages().create(
MessageCreateParams.builder()
.model("claude-sonnet-4-5")
.system(List.of(
TextBlockParam.builder()
.text(LONG_SYSTEM_PROMPT) // 5000 tokens
.cacheControl(/* ephemeral */)
.build()
))
.build()
);
// 첫 호출: 전체 과금
// 재사용 시: 시스템 프롬프트 부분 10% 과금
LLM 응답은 수 초가 걸립니다. Streaming 없이 기다리면 사용자 UX가 나빠지고, 서버에서는 긴 HTTP 커넥션을 붙잡고 있어야 합니다. SSE(Server-Sent Events)로 토큰 하나씩 흘려받는 것이 표준입니다.
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestParam String prompt) {
return anthropicClient.streamMessage(prompt)
.map(event -> event.getText())
.onErrorResume(e -> Flux.just("[ERROR]"));
}
// 프론트엔드 (JavaScript)
const es = new EventSource('/chat?prompt=...');
es.onmessage = e => {
document.getElementById('output').textContent += e.data;
};
// 429 Too Many Requests → 재시도
@Retryable(
retryFor = RateLimitException.class,
maxAttempts = 4,
backoff = @Backoff(
delay = 1000, // 1초
multiplier = 2, // 2배씩: 1→2→4→8초
maxDelay = 30000 // 최대 30초
)
)
public String callLLM(String prompt) {
return anthropicClient.call(prompt);
}
// 쿠팡 경험: Redis 카운터로 Rate Limit
// 선제 제어 (429 도달 전에 차단)
세 가지 방법이 있습니다. 첫째, 출력 토큰 제어입니다. max_tokens 파라미터로 응답 길이를 제한하고, 구조화된 출력(JSON)을 요구해 장황함을 줄입니다. 둘째, Prompt Caching입니다. 반복되는 긴 시스템 프롬프트에 Anthropic의 cache_control을 적용하면 캐시 hit 시 90% 할인됩니다. 셋째, 모델 라우팅입니다. 간단한 요청은 GPT-4o mini나 Claude Haiku같은 저렴한 모델로, 복잡한 요청만 비싼 모델로 보내는 티어드 라우팅을 씁니다.
세 가지 전략 중 상황에 맞게 선택합니다. 슬라이딩 윈도우: 가장 오래된 대화를 잘라냅니다. 간단하지만 초기 컨텍스트 유실. 요약(Summarization): 오래된 대화를 LLM으로 요약해 압축합니다. 핵심은 유지되지만 요약 비용 추가. RAG(Retrieval Augmented Generation): 대화 이력을 Vector DB에 저장하고 현재 질문과 관련된 것만 검색해서 컨텍스트에 넣습니다. 장기 대화에 적합합니다.
LLM API는 토큰 단위 과금이고 출력이 입력보다 ~5배 비싸므로, max_tokens 제한 + Prompt Caching + 모델 라우팅으로 비용을 통제해야 한다.