LLM API 기초: 요청, 스트리밍, 에러 처리
LLM API의 메시지 구조와 시스템 프롬프트, 스트리밍 응답, 재시도와 타임아웃, 키 관리까지 프로덕션에 필요한 기본기를 코드와 함께 정리합니다.

이 글의 목차
챗봇 화면에서 쓰던 모델을 코드로 호출하기 시작하면 새로운 문제들이 나타납니다. 응답이 30초 넘게 걸리고, 가끔 429 오류가 나고, 사용자는 빈 화면을 보며 기다립니다. 모델 성능과는 무관한, 순전히 통합의 문제입니다.
이 글에서는 LLM API를 프로덕션에서 쓸 때 필요한 기본기를 정리합니다. 메시지를 어떻게 구성하는지, 왜 스트리밍이 사실상 필수인지, 어떤 오류를 어떻게 다뤄야 하는지를 다룹니다.
코드 예제는 개념을 보여 주기 위한 것으로, 실제 SDK의 함수 이름과 파라미터는 제공사마다 다릅니다. 사용하는 제공사의 공식 문서를 함께 확인하세요.
요청은 메시지 배열입니다
대부분의 LLM API는 대화를 역할이 붙은 메시지의 배열로 표현합니다.
{
"model": "모델-식별자",
"max_tokens": 1024,
"system": "당신은 사내 규정을 안내하는 도우미입니다. 규정에 없는 내용은 추측하지 마세요.",
"messages": [
{ "role": "user", "content": "연차는 며칠인가요?" },
{ "role": "assistant", "content": "입사 1년 차 기준 15일입니다." },
{ "role": "user", "content": "이월은 되나요?" }
]
}핵심은 API에 상태가 없다는 것입니다. 서버는 이전 요청을 기억하지 않습니다. 대화를 이어 가려면 지금까지의 내역을 매번 다시 보내야 합니다. 이 구조 때문에 대화가 길어질수록 요청이 커지고 비용이 늘어납니다(컨텍스트 윈도우 참고).
시스템 프롬프트는 역할·규칙·출력 형식처럼 대화 전체에 적용되는 지시를 담는 자리입니다. 제공사에 따라 별도 필드이거나 system 역할 메시지입니다. 매 요청에 동일하게 들어가므로 프롬프트 캐싱의 대상이 되기도 합니다.
max_tokens 는 생성할 응답의 최대 길이입니다. 너무 작으면 답변이 문장 중간에 잘립니다. 응답이 자꾸 끊긴다면 이 값부터 확인하세요. 응답 객체의 종료 사유(stop_reason 등)를 보면 정상 종료인지 길이 초과인지 구분할 수 있습니다.
스트리밍은 선택이 아닙니다
사용자 대면 서비스라면 스트리밍을 기본으로 놓아야 합니다. 이유는 단순합니다. LLM의 동작 원리에서 설명했듯 모델은 토큰을 하나씩 순차적으로 만들어 냅니다. 긴 답변은 완성까지 수십 초가 걸릴 수 있습니다.
스트리밍을 쓰면 첫 토큰이 나오는 즉시 화면에 보여 줄 수 있습니다. 총 소요 시간은 같지만 체감 속도가 완전히 달라집니다.
const stream = await client.messages.stream({
model: process.env.MODEL_ID,
max_tokens: 1024,
messages: [{ role: 'user', content: question }]
});
for await (const event of stream) {
if (event.type === 'content_block_delta') {
process.stdout.write(event.delta.text);
}
}
const final = await stream.finalMessage(); // 사용량·종료 사유 확인스트리밍을 쓸 때 고려할 점이 있습니다.
- 중간 취소 — 사용자가 화면을 떠나면 연결을 끊어 비용을 아낄 수 있습니다.
AbortController를 연결해 두세요. - JSON 응답과는 상성이 나쁩니다 — 구조화된 출력은 완성되기 전까지 파싱할 수 없습니다. 이 경우 스트리밍 이득이 적습니다.
- 프록시 버퍼링 — 중간 서버가 응답을 모았다가 한 번에 보내면 스트리밍이 무의미해집니다. 리버스 프록시 설정을 확인하세요.
반드시 다뤄야 할 오류들
LLM API는 일반 REST API보다 실패가 잦습니다. 응답 시간이 길고, 사용량 제한이 촘촘하며, 부하에 민감합니다.
| 상황 | 원인 | 대응 |
|---|---|---|
| 429 | 사용량 제한 초과 | 지수 백오프 재시도. Retry-After 헤더가 있으면 그 값을 따름 |
| 500·502·503 | 일시적 서버 문제 | 지수 백오프 재시도 |
| 529 등 과부하 | 제공사 혼잡 | 재시도 + 대체 모델 고려 |
| 400 | 요청 형식 오류, 컨텍스트 초과 | 재시도 금지. 요청을 고쳐야 함 |
| 401 | 인증 실패 | 재시도 금지. 키 확인 |
| 타임아웃 | 응답 지연 | 타임아웃을 넉넉히. 스트리밍 사용 |
재시도할 때 지수 백오프와 지터를 함께 쓰는 것이 중요합니다. 고정 간격으로 재시도하면 여러 클라이언트가 동시에 몰려 문제가 반복됩니다.
async function callWithRetry(fn, { retries = 4, baseMs = 500 } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
const retriable = [429, 500, 502, 503, 529].includes(err.status);
if (!retriable || attempt >= retries) throw err;
const wait = baseMs * 2 ** attempt + Math.random() * 250; // 지터
await new Promise((r) => setTimeout(r, wait));
}
}
}주의
400번대 오류(401 제외 일부 예외)는 재시도해도 같은 결과입니다. 재시도 목록에 넣으면 실패를 지연시키고 비용만 늘립니다. 특히 컨텍스트 초과는 요청을 줄여야 해결됩니다.
타임아웃은 넉넉하게 잡아야 합니다. 일반 API 기준인 5~10초를 적용하면 정상 응답도 실패로 처리됩니다. 긴 응답을 다룬다면 수십 초 단위를 고려하고, 가능하면 스트리밍으로 전환하세요.
사용량과 비용을 계측합니다
대부분의 API는 응답에 토큰 사용량을 포함합니다. 이 값을 처음부터 기록하세요. 나중에 비용이 예상보다 커졌을 때 원인을 찾으려면 데이터가 있어야 합니다.
최소한 다음을 남기는 것이 좋습니다.
- 요청 식별자, 사용한 모델, 입력·출력 토큰 수
- 응답 지연 시간, 재시도 횟수, 종료 사유
- 기능·화면 단위 태그 (어느 기능이 비용을 쓰는지 구분)
토큰 수를 미리 알아야 한다면 SDK가 제공하는 토큰 계산 기능을 쓰세요. 글자 수로 추정하면 특히 한국어에서 크게 어긋납니다(토큰과 토크나이저 참고).
키 관리와 호출 위치
API 키를 브라우저나 모바일 앱에 두면 안 됩니다. 번들에 포함된 키는 누구나 추출할 수 있고, 유출되면 그대로 과금됩니다.
정석은 서버를 한 단계 두는 것입니다.
브라우저 → 우리 서버(인증·검증·제한·로깅) → LLM API이 중간 서버가 담당할 일은 다음과 같습니다.
- 사용자 인증과 권한 확인
- 사용자별 호출 횟수·토큰 제한
- 프롬프트 조립(시스템 프롬프트는 서버에서만 주입)
- 로깅과 비용 집계
- 키 보관(환경 변수나 시크릿 매니저)
키는 환경 변수로 주입하고 저장소에 커밋하지 마세요. 정기적으로 교체하고, 가능하면 용도별로 분리해 문제가 생겼을 때 영향 범위를 좁힙니다.
프로덕션 체크리스트
- 스트리밍으로 첫 응답 지연을 줄였는가
- 재시도에 지수 백오프와 지터를 적용했는가
- 400번대 오류를 재시도 대상에서 제외했는가
- 타임아웃을 응답 특성에 맞게 잡았는가
- 토큰 사용량을 기능 단위로 기록하는가
- 사용자별 호출 제한이 있는가
- 키가 클라이언트에 노출되지 않는가
- 모델 식별자를 설정값으로 분리했는가
- 모델이 실패했을 때의 대체 경로가 있는가
- 프롬프트 변경을 버전으로 관리하는가
마지막 두 항목이 특히 중요합니다. 모델 식별자를 코드에 하드코딩하면 교체할 때 배포가 필요합니다. 설정으로 빼 두면 모델 전환이나 A/B 테스트가 쉬워집니다.
흔한 실수
동기 호출로 사용자 요청을 막는 것. 웹 요청 처리 중에 30초짜리 호출을 기다리면 연결이 끊기거나 서버 스레드가 묶입니다. 스트리밍을 쓰거나, 긴 작업은 큐에 넣고 결과를 따로 전달하세요.
전체 대화를 무한정 쌓는 것. 대화 내역을 자르지 않으면 언젠가 컨텍스트 한도에 걸리고, 그 전에 비용이 먼저 문제가 됩니다. 길이 제한과 요약 전략을 처음부터 넣으세요.
응답을 그대로 신뢰하는 것. JSON을 요청했다고 항상 유효한 JSON이 오지는 않습니다. 파싱 실패를 전제로 검증과 재시도를 설계해야 합니다.
프롬프트를 코드에 흩뿌리는 것. 여러 파일에 문자열로 박아 두면 무엇을 바꿨을 때 무엇이 달라졌는지 추적할 수 없습니다. 한곳에 모으고 버전을 붙이세요. 자세한 내용은 프롬프트 설계에서 다룹니다.
정리
- LLM API는 상태가 없으므로 대화 내역을 매 요청에 다시 보내야 하며, 그만큼 비용이 늘어납니다.
- 사용자 대면 서비스에서는 스트리밍이 사실상 필수입니다. 체감 지연이 크게 줄어듭니다.
- 429·5xx는 지수 백오프로 재시도하고, 400번대는 재시도하지 말고 요청을 고쳐야 합니다.
- 타임아웃은 일반 API 기준보다 훨씬 넉넉하게 잡아야 합니다.
- 토큰 사용량을 기능 단위로 기록해 두면 비용 문제를 추적할 수 있습니다.
- API 키는 반드시 서버에 두고, 클라이언트에서 직접 호출하지 않습니다.
참고 자료
- Anthropic API — Messages — 메시지 구조와 파라미터
- Anthropic API — 스트리밍 — 스트리밍 이벤트 형식
- Anthropic API — 오류 처리 — 오류 코드와 재시도 권장 사항