LLM API 재시도, 타임아웃 및 로깅을 위한 작은 Node.js 래퍼 (Wrapper)
요약
LLM API를 실제 프로덕션 환경에서 안정적으로 사용하기 위한 Node.js 기반의 경량 래퍼(Wrapper) 구현 방법을 소개합니다. 타임아웃, 재시도 규칙, 로깅 기능을 포함하여 API 호출의 신뢰성과 관측성을 높이는 패턴을 다룹니다.
핵심 포인트
- LLM API 호출 시 타임아웃, 재시도, 로깅의 중요성 강조
- 추가 의존성 없이 fetch를 활용한 최소한의 Node.js 래퍼 구현
- 실패 원인 파악을 위한 구조화된 로그 기록 패턴 제안
- 애플리케이션 로직과 HTTP 경계를 분리하여 유지보수성 향상
대부분의 LLM API 통합은 직접적인 SDK 호출로 시작합니다.
데모용으로는 괜찮습니다.
하지만 호출이 실제 제품 내부에 들어가게 되면, 저는 보통 세 가지 기능을 주변에 두고 싶어 합니다:
- 타임아웃 (timeout)
- 재시도 규칙 (retry rules)
- 무언가 실패했을 때의 유용한 로그 (useful logs)
거대한 프레임워크가 아닙니다. 완전한 관측성 플랫폼 (observability platform)도 아닙니다. 그저 지루한 프로덕션 관련 작업들을 잊지 않게 도와주는 작은 래퍼 (wrapper)일 뿐입니다.
여기 제가 주로 사용하는 Node.js 래퍼 패턴이 있습니다.
문제점 (The problem)
일반적인 LLM 호출은 보통 다음과 같이 시작됩니다:
const response = await client.chat.completions.create({
model: "gpt-4.1-mini",
messages: [
...
읽기는 쉽지만, 몇 가지 프로덕션 관련 질문들을 숨기고 있습니다:
- 우리가 포기하기 전까지 이 요청이 얼마나 오래 실행될 수 있는가?
- 어떤 에러들을 재시도 (retry)해야 하는가?
- 몇 번이나 재시도할 것인가?
- 이 워크플로우에서 재시도가 안전한가?
- 요청이 실패했을 때 무엇을 로그 (log)로 남길 것인가?
- 나중에 누군가가 전체 요청을 재현하지 않고도 이를 디버깅할 수 있는가?
아래의 래퍼는 의도적으로 작게 설계되었습니다. 모든 LLM 신뢰성 문제를 해결하려고 시도하지 않습니다.
그저 각 요청에 기본적인 실패 정책 (failure policy)을 제공할 뿐입니다.
최소한의 래퍼 (A minimal wrapper)
이 버전은 fetch를 사용하므로, 추가 의존성 없이 현대적인 Node.js에서 작동합니다.
const DEFAULT_TIMEOUT_MS = 30_000;
function sleep(ms) {
...
사용법 (Using it)
다음은 기본적인 채팅 완료 (chat completion) 요청입니다:
import { callLlmWithPolicy } from "./llm-wrapper.js";
const result = await callLlmWithPolicy({
...
OpenAI 호환 제공업체를 사용하는 경우에도 형태는 거의 동일하게 유지됩니다. 주요 변경 사항은 대개 베이스 URL (base URL)과 모델 이름입니다.
이것이 제가 모든 로직을 애플리케이션 코드 내부에 파묻는 대신, 이 래퍼를 HTTP 경계 (HTTP boundary) 근처에 유지하는 것을 좋아하는 이유 중 하나입니다.
이 래퍼가 기록하는 로그 (What this wrapper logs)
로그는 의도적으로 단순합니다.
시작 시:
{
event: "llm_request_started",
requestId: "01...",
...
실패 시:
{
event: "llm_request_failed",
requestId: "01...",
...
성공 시:
{
event: "llm_request_succeeded",
requestId: "01...",
...
}
이 정보들은 나중에 몇 가지 중요한 질문에 답하기에 충분합니다:
- 한 번 실패했다가 복구되었는가?
- 모든 재시도 (Retries) 횟수를 소진했는가?
- 타임아웃 (Timeout)이었는가, 아니면 제공자 (Provider)의 응답이었는가?
- 어떤 모델 (Model)이 호출되었는가?
- 각 시도 (Attempt)에 시간이 얼마나 걸렸는가?
- 제공자가
Retry-After를 보냈는가?
많은 운영 환경의 디버깅 (Debugging) 상황에서, 이 정도만 되어도 "AI 호출이 실패했습니다"라는 말보다는 훨씬 낫습니다.
래퍼 (Wrapper)에서 숨기지 않을 부분
모든 LLM 호출에 대해 재시도 (Retries)를 자동으로 수행하는 것은 주의해야 합니다.
어떤 호출은 반복해도 안전합니다.
예를 들어:
- 텍스트 요약 (Summarizing text)
- 티켓 분류 (Classifying a ticket)
- 문서에서 필드 추출 (Extracting fields from a document)
- 아직 보여지거나 저장되지 않은 초안 생성 (Generating a draft)
반면, 어떤 호출은 반복하기에 안전하지 않을 수 있습니다.
예를 들어:
- 이메일 전송 (Sending an email)
- 티켓 생성 (Creating a ticket)
- 고객 결제 (Charging a customer)
- 웹훅 (Webhook) 호출
- 에이전트 (Agent)가 도구 (Tool)를 실행하도록 허용
래퍼 (Wrapper)는 HTTP 상태 코드 (Status code)만으로는 이를 알 수 없습니다.
그래서 실제 애플리케이션에서는 보통 다음과 같은 입력값을 하나 더 추가합니다:
retryMode: "safe" | "unsafe"
그러면 재시도 규칙이 더 엄격해집니다:
function shouldRetry({ status, error, attempt, maxRetries, retryMode }) {
if (attempt >= maxRetries) return false;
...
이 작은 차이가 중요합니다.
읽기 전용인 요약 호출에서의 재시도는 별것 아닙니다.
하지만 에이전트가 이미 외부 동작을 트리거했을지도 모르는 상황에서의 재시도는 완전히 다른 실패 모드 (Failure mode)입니다.
타임아웃 (Timeouts)에는 제품적 판단이 필요합니다
저는 보통 모든 LLM 호출에 대해 하나의 전역 타임아웃 (Global timeout)을 사용하는 것을 피합니다.
워크플로 (Workflows)마다 허용 오차가 다르기 때문입니다.
예를 들어:
const timeoutByWorkflow = {
autocomplete: 5_000,
support_summary: 20_000,
...
사용자 대면용 자동 완성 (Autocomplete)은 아마도 빠르게 실패 (Fail fast)해야 할 것입니다.
백그라운드 분석 작업 (Background analysis job)은 더 오래 기다릴 수 있습니다.
고객 지원 요약 (Support summary)은 그 중간 어디쯤 위치할 수 있습니다.
핵심은 완벽한 타임아웃을 찾는 것이 아닙니다. 타임아웃 동작이 우연이 아닌 의도적으로 이루어지게 만드는 것이 핵심입니다.
래퍼 (Wrapper)를 지루하게 유지하세요
기능을 계속해서 추가하고 싶은 유혹이 생길 수 있습니다:
- 모델 폴백 (model fallback)
- 스키마 검증 (schema validation)
- 스트리밍 복구 (streaming recovery)
- 서킷 브레이커 (circuit breakers)
- 제공자 라우팅 (provider routing)
- 프롬프트 레드액션 (prompt redaction)
- 토큰 계정 관리 (token accounting)
이 모든 것들은 유용할 수 있습니다.
하지만 저는 먼저 지루한 계층부터 시작하는 것을 선호합니다:
- 각 요청에 대한 타임아웃 (timeout) 설정
- 알려진 일시적 실패 (transient failures)에 대해서만 재시도
Retry-After헤더 준수- 모든 시도 로그 기록 (log)
- 안전하지 않은 작업에 대한 재시도 방지
- 요청 ID (request ID) 첨부
이것은 첫 번째 장애가 발생한 후가 아니라, 발생하기 전에 갖춰두어야 할 부분입니다.
마지막 생각
작은 LLM 래퍼 (wrapper) 하나가 당신의 앱을 그 자체로 프로덕션 수준(production-ready)으로 만들어주지는 않습니다.
하지만 그것은 프로덕션 정책 (production policy)이 거주할 수 있는 공간을 만들어줍니다.
이러한 계층이 없다면, 재시도 동작은 라우트 핸들러 (route handlers), 백그라운드 작업 (background jobs), SDK 호출, 그리고 에이전트 도구 (agent tools) 전반에 흩어지게 됩니다. 결국, 어떤 LLM 호출이 재시도해도 안전한지, 그리고 어떤 호출이 조용히 위험한지 아무도 알 수 없게 됩니다.
이제 저의 규칙은 간단합니다:
만약 LLM 호출이 제품에 중요하다면, 그것은 가공되지 않은(naked) SDK 호출이어서는 안 됩니다.
그 주변에 작은 정책을 두십시오.
그러면 미래의 디버깅 세션이 더 짧아질 것입니다.
저는 TokenBay에서 일하고 있으므로, OpenAI 호환 API, 멀티 모델 라우팅 (multi-model routing), 재시도 및 실패 처리 (failure handling)에 대해 많은 시간을 할애하여 고민합니다. 위의 래퍼는 의도적으로 제공자 불가지론적 (provider-agnostic)으로 설계되었습니다. 즉, 하나의 제공자에게 직접 호출하든 호환 가능한 API 게이트웨이를 통해 라우팅하든 동일한 기본 구조가 작동합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기