1. LLM API & 토큰 경제학

LLM API를 서버에서 호출하는 건 일반 REST API 호출과 다릅니다. 토큰 과금, Context Window 한계, Rate Limit, Streaming을 이해해야 제대로 된 AI 서비스를 만들 수 있습니다.

1. 토큰이란? — LLM의 기본 단위

LLM은 텍스트를 문자나 단어 단위로 처리하지 않고 토큰(Token) 단위로 처리합니다. 토큰은 단어보다 작은 단위로, 영어는 평균 1토큰 ≈ 0.75단어, 한국어는 1토큰 ≈ 0.3~0.5글자입니다.

"Hello, backend developer!" → 토큰 분리
Hello , backend developer !
한국어: "안녕하세요" → (글자당 1~2토큰)
Context Window — LLM이 한 번에 볼 수 있는 양

Context Window는 LLM이 한 번의 요청에서 처리할 수 있는 최대 토큰 수입니다. 입력(프롬프트 + 대화 이력)과 출력(응답)을 합친 총량이 이 한도를 초과할 수 없습니다.
Claude 3.7 Sonnet: 200K 토큰 (약 책 150페이지 분량)
GPT-4o: 128K 토큰
Context가 꽉 차면 이전 대화를 잘라내거나 요약해야 합니다. 이 전략을 Context Management라 합니다.

2. 토큰 과금 구조 — 입력과 출력이 다르다

모델입력 토큰 (1M당)출력 토큰 (1M당)특이사항
Claude Sonnet 4.5$3.00$15.00Prompt Caching 지원 (90% 할인)
GPT-4o$2.50$10.00Vision 포함
GPT-4o mini$0.15$0.60저비용 라우팅용
Gemini 1.5 Pro$1.25$5.002M Context
비용 함정 — 출력이 입력보다 5배 비쌈
// 잘못된 프롬프트 설계
String prompt = "이 코드를 리뷰해줘. 가능한 한 상세하게 설명해줘.";
// → 모델이 장황하게 출력 → 비용 폭발

// 출력 토큰 제한
request.setMaxTokens(500); // 필수!

// 구조화된 출력 요구
String prompt = "다음 JSON 형식으로만 답해줘: " +
    "{\"issues\": [...], \"score\": 0-10}";
Prompt Caching — 반복 입력 90% 절약
// 긴 시스템 프롬프트가 매 요청마다 반복되면 낭비
// 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% 과금

3. Streaming — 응답을 기다리지 말고 흘려받아라

LLM 응답은 수 초가 걸립니다. Streaming 없이 기다리면 사용자 UX가 나빠지고, 서버에서는 긴 HTTP 커넥션을 붙잡고 있어야 합니다. SSE(Server-Sent Events)로 토큰 하나씩 흘려받는 것이 표준입니다.

Streaming 처리 (Java + Spring WebFlux)
@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;
};
Rate Limit 대응 — Exponential Backoff
// 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 도달 전에 차단)

4. 면접 대비 핵심 Q&A

Q1. "LLM API 호출 비용을 줄이는 방법은?"

세 가지 방법이 있습니다. 첫째, 출력 토큰 제어입니다. max_tokens 파라미터로 응답 길이를 제한하고, 구조화된 출력(JSON)을 요구해 장황함을 줄입니다. 둘째, Prompt Caching입니다. 반복되는 긴 시스템 프롬프트에 Anthropic의 cache_control을 적용하면 캐시 hit 시 90% 할인됩니다. 셋째, 모델 라우팅입니다. 간단한 요청은 GPT-4o mini나 Claude Haiku같은 저렴한 모델로, 복잡한 요청만 비싼 모델로 보내는 티어드 라우팅을 씁니다.

Q2. "Context Window가 가득 차면 어떻게 처리하나요?"

세 가지 전략 중 상황에 맞게 선택합니다. 슬라이딩 윈도우: 가장 오래된 대화를 잘라냅니다. 간단하지만 초기 컨텍스트 유실. 요약(Summarization): 오래된 대화를 LLM으로 요약해 압축합니다. 핵심은 유지되지만 요약 비용 추가. RAG(Retrieval Augmented Generation): 대화 이력을 Vector DB에 저장하고 현재 질문과 관련된 것만 검색해서 컨텍스트에 넣습니다. 장기 대화에 적합합니다.

핵심 한 줄

LLM API는 토큰 단위 과금이고 출력이 입력보다 ~5배 비싸므로, max_tokens 제한 + Prompt Caching + 모델 라우팅으로 비용을 통제해야 한다.