Claude API

zion·5일 전

🤖 이 글은 Claude Code을 활용하여 작성되었습니다.


타이핑되듯 흘러나오는 응답. 그게 구현 편의가 아니라 모델의 작동 방식에서 비롯된 것이라면?

Claude API를 처음 쓸 때 대부분 스트리밍 여부를 옵션 정도로 생각한다. 하지만 stream=True 한 줄의 근거는 HTTP 프로토콜 선택을 넘어, 모델이 텍스트를 생성하는 구조 자체에 있다. 통신 계층부터 Transformer 추론까지, 하나의 요청이 완성되는 흐름을 분해한다.

TL;DR

  • Claude API는 REST + JSON. gRPC 없음
  • 응답은 일반(JSON 한 번에)과 스트리밍(SSE) 두 가지
  • 스트리밍이 자연스러운 이유: 모델이 토큰을 하나씩 생성하기 때문
  • Transformer의 Attention → Autoregressive 생성 → SSE 전송이 하나의 흐름

1️⃣ 통신 구조 — REST + SSE

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와 세션 인증의 차이를 설명해줘"}
  ]
}

응답 방식은 두 가지다.

일반 응답 vs 스트리밍 응답

방식동작적합한 상황
일반 (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)

gRPC를 선택하지 않은 이유

LLM API의 통신 패턴은 단방향 요청-응답에 서버 푸시 스트림이 전부다. gRPC의 장점이 발휘될 자리가 없다.

비교 항목REST + SSEgRPC
브라우저 직접 호출❌ (프록시 필요)
curl 디버깅번거로움
클라이언트 코드 생성불필요필요
스트리밍SSE로 충분양방향 가능하나 불필요

WebSocket 등 다른 선택지도 있지만, SSE는 HTTP 위에서 동작해 별도 인프라 없이 어디서나 호출 가능하다는 점에서 가장 단순한 선택이 된다.


2️⃣ 모델 내부 구조 — 토큰이 만들어지는 과정

토큰: 모델이 보는 언어의 단위

Claude는 텍스트를 문자가 아닌 토큰 단위로 처리한다. 자주 등장하는 문자 조합을 하나의 토큰으로 묶는 방식으로, 사용하는 토크나이저에 따라 분리 방식이 달라진다.

"Hello, world!" → ["Hello", ",", " world", "!"]  (약 4토큰)
"안녕하세요"    → ["안녕", "하세", "요"]           (약 3토큰, 토크나이저마다 다름)

영어는 평균 4글자당 1토큰, 한국어는 구조상 토큰 밀도가 더 높은 편이다. 입력과 출력 모두 토큰 단위로 비용이 계산된다. max_tokens가 출력 토큰 수를 제한하는 파라미터인 이유가 여기 있다.


Transformer: 맥락을 읽는 구조

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: 확률 분포를 조절하는 파라미터

각 단계에서 다음 토큰을 고를 때 확률 분포를 참조한다. temperaturetop_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이 참조하는 범위

Attention은 컨텍스트 창 안의 모든 토큰을 동시에 참조한다. Claude Sonnet 4.6과 Opus 4.6 모두 최대 1M 토큰을 지원한다 (2026년 3월 GA).

[시스템 프롬프트] [이전 대화 히스토리] [현재 질문]
|←──────────────── 컨텍스트 창 (1M 토큰) ────────────────→|

창이 가득 차면 오래된 내용부터 참조 불가 상태가 된다. Compaction API는 이 한계를 서버 사이드 요약으로 우회한다.


3️⃣ 전체 흐름 — 요청에서 응답까지

[클라이언트]
    │  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 옵션이 아니라 모델이 동작하는 방식에 대한 이해에서 나온다. 인터페이스를 외우는 것과 구조를 이해하는 것의 차이가 여기서 드러난다.

profile
be_zion

0개의 댓글