🤖 이 글은 Claude Code을 활용하여 작성되었습니다.
타이핑되듯 흘러나오는 응답. 그게 구현 편의가 아니라 모델의 작동 방식에서 비롯된 것이라면?
Claude API를 처음 쓸 때 대부분 스트리밍 여부를 옵션 정도로 생각한다. 하지만 stream=True 한 줄의 근거는 HTTP 프로토콜 선택을 넘어, 모델이 텍스트를 생성하는 구조 자체에 있다. 통신 계층부터 Transformer 추론까지, 하나의 요청이 완성되는 흐름을 분해한다.
TL;DR
Claude API는 HTTPS REST 기반이다. 모든 요청은 단일 엔드포인트로 수렴한다.
POST https://api.anthropic.com/v1/messages
Content-Type: application/json
x-api-key: sk-ant-...
anthropic-version: 2023-06-01
{
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "JWT와 세션 인증의 차이를 설명해줘"}
]
}
응답 방식은 두 가지다.
| 방식 | 동작 | 적합한 상황 |
|---|---|---|
| 일반 (JSON) | 생성 완료 후 전체를 한 번에 반환 | 짧은 응답, 배치 처리 |
| 스트리밍 (SSE) | 생성 즉시 청크 단위로 전송 | 긴 응답, 실시간 UI |
일반 응답은 완성된 JSON을 한 방에 받는다.
{
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"content": [{"type": "text", "text": "JWT는 stateless..."}],
"usage": { "input_tokens": 20, "output_tokens": 340 }
}
스트리밍 응답은 Server-Sent Events로 토큰이 생성되는 즉시 흘러나온다.
data: {"type": "content_block_delta", "delta": {"text": "JWT"}}
data: {"type": "content_block_delta", "delta": {"text": "는"}}
data: {"type": "content_block_delta", "delta": {"text": " stateless"}}
data: {"type": "message_stop"}
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "JWT와 세션 인증의 차이를 설명해줘"}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
LLM API의 통신 패턴은 단방향 요청-응답에 서버 푸시 스트림이 전부다. gRPC의 장점이 발휘될 자리가 없다.
| 비교 항목 | REST + SSE | gRPC |
|---|---|---|
| 브라우저 직접 호출 | ✅ | ❌ (프록시 필요) |
| curl 디버깅 | ✅ | 번거로움 |
| 클라이언트 코드 생성 | 불필요 | 필요 |
| 스트리밍 | SSE로 충분 | 양방향 가능하나 불필요 |
WebSocket 등 다른 선택지도 있지만, SSE는 HTTP 위에서 동작해 별도 인프라 없이 어디서나 호출 가능하다는 점에서 가장 단순한 선택이 된다.
Claude는 텍스트를 문자가 아닌 토큰 단위로 처리한다. 자주 등장하는 문자 조합을 하나의 토큰으로 묶는 방식으로, 사용하는 토크나이저에 따라 분리 방식이 달라진다.
"Hello, world!" → ["Hello", ",", " world", "!"] (약 4토큰)
"안녕하세요" → ["안녕", "하세", "요"] (약 3토큰, 토크나이저마다 다름)
영어는 평균 4글자당 1토큰, 한국어는 구조상 토큰 밀도가 더 높은 편이다. 입력과 출력 모두 토큰 단위로 비용이 계산된다. max_tokens가 출력 토큰 수를 제한하는 파라미터인 이유가 여기 있다.
Claude는 Transformer 기반으로 알려진 대규모 언어 모델이다 (Anthropic이 내부 아키텍처를 공식 공개하지는 않았다). 핵심은 Attention 메커니즘으로, 입력된 모든 토큰이 서로 얼마나 관련 있는지를 동시에 계산한다.
입력: "주문이 완료되면 [?]을 발송한다"
↑
Attention이 "주문", "완료", "발송"과의 관계에
높은 가중치를 부여하고, 이를 바탕으로
모델이 "메일"을 높은 확률로 예측
Attention은 토큰 간 관계 가중치를 계산하는 레이어이며, 실제 다음 토큰 예측은 이후 FFN(Feed-Forward Network)과 Softmax 레이어가 담당한다. 이 계산이 컨텍스트 창 전체에 걸쳐 일어나기 때문에, 수만 줄의 코드베이스도 앞뒤 맥락을 고려한 답변이 가능하다.
Claude가 텍스트를 생성하는 방식은 다음 토큰을 하나씩 예측하는 반복이다.
입력: "JWT는"
1단계: "JWT는" → 예측 → "stateless"
2단계: "JWT는 stateless" → 예측 → "방식으로"
3단계: "JWT는 stateless 방식으로" → 예측 → "..."
전체 응답을 미리 완성해두고 보내는 게 아니다. 토큰을 생성하는 즉시 전송하기 때문에 SSE가 이 구조와 정확히 맞물린다.
각 단계에서 다음 토큰을 고를 때 확률 분포를 참조한다. temperature와 top_p는 이 분포를 조절하는 파라미터다.
| 파라미터 | 낮은 값 | 높은 값 |
|---|---|---|
temperature | 가장 높은 확률 토큰만 선택 (결정적) | 확률 분포 그대로 샘플링 (다양함) |
top_p | 상위 확률 토큰 중에서만 선택 (보수적) | 전체 후보 중 선택 (개방적) |
실제 사용에서는 용도에 따라 명확히 나뉜다.
import anthropic
client = anthropic.Anthropic()
# 코드 생성 — 정확성 우선, 낮은 temperature
code_response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
temperature=0.1, # 거의 결정적. 같은 입력 → 같은 출력
messages=[{"role": "user", "content": "Python으로 이진 탐색 구현해줘"}]
)
# 블로그 초안 작성 — 다양성 우선, 높은 temperature
creative_response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
temperature=0.9, # 매번 다른 표현. 창의적 글쓰기에 적합
messages=[{"role": "user", "content": "Redis를 소개하는 블로그 도입부를 써줘"}]
)
두 파라미터를 동시에 설정할 때 적용 순서가 있다. temperature가 먼저 로짓(logits)의 날카로움을 조정하고, 그 결과로 만들어진 확률 분포에서 top_p가 후보군을 좁힌 뒤 최종 샘플링이 이루어진다. 일반적으로는 하나만 조정하는 것이 결과를 예측하기 쉽다.
Attention은 컨텍스트 창 안의 모든 토큰을 동시에 참조한다. Claude Sonnet 4.6과 Opus 4.6 모두 최대 1M 토큰을 지원한다 (2026년 3월 GA).
[시스템 프롬프트] [이전 대화 히스토리] [현재 질문]
|←──────────────── 컨텍스트 창 (1M 토큰) ────────────────→|
창이 가득 차면 오래된 내용부터 참조 불가 상태가 된다. Compaction API는 이 한계를 서버 사이드 요약으로 우회한다.
[클라이언트]
│ HTTPS POST /v1/messages (JSON)
▼
[api.anthropic.com]
│ 1. 인증 확인 (x-api-key)
│ 2. 입력 토크나이징
│ 3. Transformer 추론 시작
│ ├─ 토큰 생성 → SSE 전송 → [클라이언트] 표시
│ ├─ 다음 토큰 생성 → SSE 전송 → [클라이언트] 표시
│ └─ (max_tokens 도달 또는 EOS 토큰까지 반복)
│ 4. message_stop 이벤트 전송
▼
[연결 종료]
REST냐 gRPC냐의 선택은 프로토콜 취향이 아니다. 모델이 토큰을 하나씩 생성한다는 구조적 사실이 SSE를 가장 단순한 선택으로 만들고, SSE가 HTTP 위에서 동작한다는 점이 REST와 자연스럽게 맞물린다.
max_tokens를 어떻게 설정할지, 스트리밍을 쓸지 말지, temperature를 얼마로 줄지. 이 결정들은 API 옵션이 아니라 모델이 동작하는 방식에 대한 이해에서 나온다. 인터페이스를 외우는 것과 구조를 이해하는 것의 차이가 여기서 드러난다.