OpenAI API를 프로덕션 Express 앱에 통합하는 방법
요약
OpenAI API를 실제 서비스 환경에 통합할 때 발생하는 비용 폭증 문제를 다룹니다. 단순 튜토리얼을 넘어 속도 제한, 토큰 계산, 비용 방어 등 프로덕션 수준의 안정적인 Express 앱 구축 방법을 가이드합니다.
핵심 포인트
- 단순 API 호출과 프로덕션 환경의 차이 이해
- 비용 폭증을 막기 위한 토큰 계산 및 지출 한도 설정
- Rate Limiting을 통한 무분별한 API 요청 방어
- 에러 핸들링, 스트리밍, 모델 선택의 중요성
지난해 저는 한 스타트업이 그들의 제품에 OpenAI API를 통합하는 것을 도왔습니다. 그것은 채팅 기능이었는데, 사용자들이 자신의 데이터에 대해 질문하면 자연어 답변을 받을 수 있는 기능이었습니다. 통합 작업은 약 하루 정도 걸렸습니다. 출시 3일 후, 창업자가 저에게 메시지를 보냈습니다: "저기, 뭔가 잘못됐어요. 우리 AWS 청구서에 예상치 못한 비용이 찍혔어요."
그 금액은 340달러였습니다. 단 3일 동안 말이죠. 사용자 수는 60명이었습니다.
문제는 버그가 아니었습니다. 프로덕션 (Production) 환경에서의 API 사용은 튜토리얼과는 전혀 다르다는 점이었습니다. 튜토리얼은 openai.chat.completions.create()를 호출하고 응답을 반환하는 법을 보여줍니다. 하지만 튜토리얼은 사용자가 500토큰 (token) 메시지를 보낼 때, 사용자가 각각 자신의 채팅 컨텍스트 (context)를 유지하는 브라우저 탭을 15개나 열어둘 때, 또는 한 사용자가 서비스가 고장 났다고 생각하여 분당 30번씩 요청을 보낼 때 어떤 일이 발생하는지는 보여주지 않습니다.
이 가이드는 튜토리얼에서 생략된 내용들을 다룹니다: 속도 제한 (rate limiting), 토큰 계산 (token counting), 비용 방어 (cost guards), 스트리밍 (streaming), 재시도를 포함한 에러 핸들링 (error handling), 그리고 모델 선택 (model selection)입니다. 이것들은 선택적인 추가 사항이 아닙니다. 데모와 프로덕션 기능을 구분 짓는 핵심 요소입니다.
프로덕션 환경이 다른 이유
튜토리얼 코드와 프로덕션 코드 사이의 차이점을 명확하게 정리하면 다음과 같습니다:
| 고려 사항 | 튜토리얼 코드 | 프로덕션 코드 |
|---|---|---|
| 비용 제어 | 언급되지 않음 | 토큰 계산, 지출 한도, 작업별 모델 선택 |
| ... |
이 모든 문제를 해결하는 프로덕션급 Express API를 구축해 보겠습니다. 한 단계씩 차근차근 진행하겠습니다.
아키텍처 (Architecture)
아키텍처 (Architecture)
┌─────────────────────────────────────────────────────────┐
│ 클라이언트 (브라우저 / 모바일) │
│ POST /api/chat { messages: [...] } │
│ GET /api/chat/stream (SSE) │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ EXPRESS 미들웨어 스택 │
│ │
│ 1. express-rate-limit (IP당 분당 10회 요청) │
│ 2. tokenGuard() (4,000 토큰 초과 시 거부) │
│ 3. auth middleware (사용자 세션 확인) │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 라우트 핸들러 │
│ │
│ 작업 유형별 모델 선택 │
│ 컨텍스트로부터 메시지 배열 구성 │
│ openai.chat.completions.create() 호출 │
│ 응답 스트리밍 또는 반환 │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ OPENAI API │
│ 모델: gpt-4o-mini (기본값) / gpt-4o (복잡한 작업) │
└─────────────────────────────────────────────────────────┘
프로젝트 설정
mkdir express-openai && cd express-openai
npm init -y
npm install express openai express-rate-limit tiktoken dotenv
...
# .env
OPENAI_API_KEY=sk-proj-your-key-here
PORT=3001
1단계: OpenAI 클라이언트 (프로덕션용으로 구성)
라우트 핸들러 내부에서 OpenAI 클라이언트를 인스턴스화하지 마세요. 한 번 생성하고, 프로덕션을 위해 구성한 다음, 내보내기(export)하세요:
// src/openaiClient.js
import OpenAI from "openai";
...
maxRetries: 3 및 timeout 설정은 매우 중요합니다. 타임아웃(timeout) 설정이 없으면, 중단된 OpenAI 요청이 Express 서버의 응답 객체(response object)를 무기한 열어두게 됩니다. 만약 서버리스 함수(serverless function)에서 실행 중이라면, 해당 유휴 시간(idle time)에 대한 비용을 지불하게 됩니다.
2단계: 토큰 계산 및 비용 방어 (Token Counting and Cost Guard)
tiktoken 라이브러리는 OpenAI 자체 토크나이저(tokenizer)로, API가 사용하는 것과 정확히 동일한 방식으로 토큰을 계산합니다. 이를 사용하여 요청이 API에 도달하기 전에 거부(reject)하십시오:
// src/tokenCounter.js
import { encoding_for_model } from "tiktoken";
...
제한 사항에 대한 참고 사항: GPT-4o-mini의 컨텍스트 윈도우(context window)는 128K 토큰이므로, 4,000개는 보수적인 수치입니다. 하지만 여기서는 보수적인 것이 좋습니다. 한 번의 요청에 30,000개의 토큰을 보내는 사용자는 특이한 동작을 수행 중이거나 클라이언트에 버그가 있는 것입니다. 이를 거부하고, 로그를 남기고, 사용자에게 컨텍스트를 비우도록 알리십시오.
3단계: 속도 제한 (Rate Limiting)
한 명의 사용자가 API 예산을 모두 소진하거나 다른 모든 사용자를 위한 OpenAI 속도 제한(rate limits)을 유발해서는 안 됩니다. AI 라우트(routes) 이전에 속도 제한을 추가하십시오:
// src/middleware/rateLimiter.js
import rateLimit from "express-rate-limit";
...
4단계: 타입화된 OpenAI 에러를 사용한 에러 핸들링 (Error Handling with Typed OpenAI Errors)
OpenAI Node SDK는 타입화된 에러(typed errors)를 발생시킵니다. 단순히 err.message만 확인하지 말고 이를 활용하십시오:
// src/middleware/openaiErrorHandler.js
import OpenAI from "openai";
...
5단계: 채팅 엔드포인트 (비스트리밍) (The Chat Endpoint (Non-Streaming))
먼저 표준 비스트리밍(non-streaming) 응답을 위해 모든 것을 연결해 보겠습니다:
// src/routes/chat.js
import express from "express";
import { openai, MODELS } from "../openaiClient.js";
...
max_tokens: 1_000에 주목하십시오. 이 설정이 없으면 GPT-4o는 요청당 4,096개의 출력 토큰을 생성할 수 있습니다. 만약 사용자가 "책 한 권을 써줘"라고 요청하면, 모델은 실제로 시도할 것입니다. max_tokens 제한은 여러분의 최후의 방어선(backstop)입니다.
6단계: 서버 전송 이벤트(Server-Sent Events)를 이용한 스트리밍 응답
스트리밍 (Streaming)은 AI 기능이 반응성이 좋다고 느껴지게 만듭니다. 3~8초 동안 빈 화면을 보여주는 대신, 사용자는 텍스트가 단어 하나하나 나타나는 것을 보게 됩니다. 이는 "AI 기반처럼 느껴지는 것"과 "고장 난 것처럼 느껴지는 것"의 차이입니다.
// src/routes/chat-stream.js
import express from "express";
import { openai, MODELS } from "../openaiClient.js";
...
시청하기: Node.js + Express를 이용한 OpenAI API
스트리밍 (Streaming) vs. 비스트리밍 (Non-Streaming) — 언제 무엇을 사용할 것인가
| 요소 | 비스트리밍 (Non-Streaming) | 스트리밍 (Streaming, SSE) |
|---|---|---|
| 사용자 경험 | 완료될 때까지 빈 화면 유지 (3~8초) | 텍스트가 단어별로 나타남 — 즉각적인 느낌 |
| ... |
OpenAI 통합 테스트하기
테스트에서 OpenAI API를 모킹 (Mocking)하는 것은 함정입니다. 모킹은 통과할지 모르지만, 실제 통합 과정에서는 예상치 못한 방식으로 실패할 수 있습니다. 예를 들어, 서로 다른 에러 형식, 예상치 못한 토큰 사용량, 스트리밍 청크 (chunk) 구조의 변동 등이 있습니다.
대신 다음과 같이 하세요:
- API 호출을 제외한 모든 것을 단위 테스트 (Unit test) 하세요. OpenAI를 건드리지 않고도 토큰 계산, 에러 핸들러 (error handler), 응답 포맷터 (response formatter) 등을 테스트하세요. 이러한 함수들은 순수 함수 (pure function) 이며 결정론적 (deterministic) 이어야 합니다.
- 통합 테스트에는 저렴한 모델을 사용하세요.
gpt-4o-mini는 입력 토큰 100만 개당 0.15달러입니다. 여러분의 통합 테스트 스위트 (test suite)를 실행하는 비용은 아마도 1센트의 아주 작은 부분일 것입니다. 그냥 실행하세요. - 비용이 많이 드는 테스트는 기록 및 재생 (Record and replay) 하세요.
nock이나 VCR 스타일의 기록 라이브러리를 사용하면 실제 API 응답을 기록하고, 향후 테스트 실행 시 API를 호출하지 않고도 이를 재생할 수 있습니다.
// 예시: 토큰 가드 (token guard) 미들웨어를 격리하여 테스트하기
import { tokenGuard } from "../src/tokenCounter.js";
import { createMockMiddlewareContext } from "./helpers.js";
...
요약 (TL;DR)
요약 (TL;DR)
- OpenAI 클라이언트를 한 번만 초기화하세요.
maxRetries와timeout을 설정하여 초기화해야 합니다. 라우트 핸들러(route handlers) 내에서 인스턴스를 생성하지 마세요. 그렇게 하면 재시도(retry)나 타임아웃(timeout) 설정이 적용되지 않은 새로운 클라이언트가 요청마다 생성됩니다. - API를 호출하기 전에 토큰을 계산하세요.
tiktoken을 사용하여 입력 크기를 측정하고, 비용이 발생하기 전에 너무 큰 요청은 거부하세요. 같은 이유로 출력에 대한max_tokens상한을 설정하십시오. - IP가 아닌 사용자 ID(user ID)로 속도 제한(Rate limit)을 거세요. 동일한 IP를 사용하는 인증된 사용자들(기업용 NAT, 모바일 네트워크 등)은 모두 하나의 IP 제한을 공유하게 됩니다. 속도 제한 키(rate limit key)로 사용자 ID를 사용하세요.
- 타입화된 에러 핸들링(typed error handling)을 사용하세요.
instanceof OpenAI.APIError를 사용하면 상태 코드(status code), 요청 ID(request ID), 메시지를 확인할 수 있습니다. 429 에러를 500 에러로 처리하지 말고, 사용자 친화적인 재시도 안내로 전환하세요. - 사용자 대상 기능에는 스트리밍(Stream)을 사용하고, 내부 호출에는 생략하세요. SSE 스트리밍(SSE streaming)은 채팅 인터페이스의 사용자 경험(UX)을 혁신적으로 변화시킵니다. 배치 처리(batch processing)나 API 간 호출(API-to-API calls)의 경우에는 스트리밍을 사용하지 않는 것이 구현과 로그 기록 면에서 더 간단합니다.
- API 호출을 제외한 모든 것을 테스트하세요. 토큰 계산, 에러 핸들링, 응답 포맷팅은 모두 저렴하게 테스트할 수 있는 순수 함수(pure functions)입니다. 통합 테스트(integration tests)를 위해서는
gpt-4o-mini를 사용하세요. CI 환경에서 실행하기에 충분히 저렴합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기