Rust로 프로덕션급 AI Gateway를 구축하며 배운 점들
요약
OpenAI, Anthropic, Ollama 등 다양한 LLM 트래픽을 관리하기 위해 Rust로 구축한 고성능 AI 게이트웨이를 소개합니다. 인증, 속도 제한, 비용 추적 및 관측성 기능을 갖추었으며, GC 오버헤드를 최소화하여 P99 지연 시간을 1.2ms로 유지합니다.
핵심 포인트
- Rust를 사용하여 GC로 인한 지연 시간 문제를 해결하고 P99 1.2ms 달성
- OpenAI 호환 API로 기존 코드의 base_url 변경만으로 즉시 적용 가능
- Redis 기반 속도 제한 및 PostgreSQL을 통한 상세 사용량 감사 기능 제공
- Prometheus와 Grafana를 활용한 실시간 메트릭 및 관측성 확보
요약 (TL;DR) — 저는 OpenAI, Anthropic (Claude), Ollama로 트래픽을 라우팅하며 인증(Auth), Redis 속도 제한(Rate limiting), PostgreSQL 사용량 추적, Prometheus 메트릭, AWS Terraform 배포 기능을 갖춘 프로덕션 준비 완료된 분산형 API 게이트웨이(Gateway)를 Rust로 구축했습니다. 게이트웨이 오버헤드는 ~1.2ms P99입니다. 그 이유와 방법을 소개합니다.
👉 GitHub: MihirMohapatra/rust-ai-gateway
내가 계속 마주쳤던 문제
6개월 전, 우리 팀은 모든 서비스에서 OpenAI를 직접 사용하고 있었습니다. 그러다 일부 작업에 Claude 3.5 Sonnet을 테스트해보고 싶어졌습니다. 그 후 컴플라이언스(Compliance) 팀에서 질문했습니다. "우리가 지금까지 보낸 모든 AI 요청을 보여줄 수 있나요?" 그러다 누군가 배치 작업(Batch job)을 실행했고, 우리는 예상치 못한 청구서를 받게 되었습니다.
익숙한 상황인가요?
진짜 문제는 특정 API가 아니었습니다. 우리 AI 트래픽을 관리할 **제어 평면 (Control plane)**이 없었다는 점이었습니다. 통합된 인증(Auth)도, 팀별 속도 제한(Rate limiting)도, 비용 가시성도 없었으며, 애플리케이션 코드를 수정하지 않고 공급업체(Provider)를 교체할 수 있는 능력도 없었습니다.
기존 솔루션들을 살펴보았습니다. 대부분 Python 기반(LiteLLM)이거나, Go 기반(Kong), 또는 SaaS(Portkey)였습니다. Rust로 된 것은 없었습니다. 구독 없이 단일 배포 가능한 바이너리(Binary) 안에 제가 원하는 전체 스택 — 인증, 속도 제한, 관측성(Observability), 멀티 프로바이더 라우팅, 클라우드 IaC — 을 모두 갖춘 것은 없었습니다.
그래서 직접 만들었습니다.
기능 요약 (10초 버전)
Your App → AI Gateway → OpenAI (GPT-4, o1, o3)
→ Anthropic (Claude 3.5 Sonnet, Opus, Haiku)
→ Ollama (Llama3, Mistral, local models)
이것은 **OpenAI와 호환되는 교체 가능한 솔루션 (Drop-in replacement)**입니다. 기존 코드에서 base_url 한 줄만 변경하면 즉시 다음 기능들을 사용할 수 있습니다:
- 키별 API 인증 (Argon2id 해싱 적용)
- Redis 슬라이딩 윈도우 속도 제한 (Sliding window rate limiting)
- PostgreSQL을 통한 전체 요청/토큰 감사 추적 (Audit trail)
- Prometheus 메트릭 + 사전 구축된 Grafana 대시보드
X-Request-ID를 통한 분산 트레이싱 (Distributed tracing)- Terraform을 이용한 Docker + AWS ECS Fargate 배포
왜 Rust인가? (진짜 이유)
저는 Java, Kotlin, Python, Go로 서비스를 배포해 왔습니다. "왜 Rust인가"라는 질문에 대한 솔직한 답변은 이상주의 때문이 아닙니다. 바로 이 특정 문제가 GC (Garbage Collection) 일시 중단(pause)에 의해 큰 타격을 입기 때문입니다.
AI 게이트웨이는 모든 LLM 호출의 핫 패스(hot path)에 위치합니다. 애플리케이션이 50개의 동시 요청을 보낼 때, 게이트웨이의 꼬리 지연 시간(tail latency)은 곧 애플리케이션의 꼬리 지연 시간이 됩니다. 이러한 시나리오에서는 P99에서의 GC 유발 일시 중단이 심각하게 누적됩니다.
Rust 버전을 구축한 후 측정한 결과는 다음과 같습니다:
| 지표 | Rust (본 프로젝트) | 일반적인 Node.js | 일반적인 Go |
|---|---|---|---|
| 메모리 (유휴 상태) | 12 MB | 80–150 MB | 20–30 MB |
| ... |
12 MB의 유휴 상태 메모리 점유율은 이 서비스를 가장 저렴한 ECS Fargate 태스크(0.5 vCPU / 1 GB RAM)에서 실행하면서도, 수백 개의 동시 요청을 처리할 수 있는 여유 공간을 확보할 수 있음을 의미합니다.
하지만 진정한 승리는 **동시성 상황에서의 정확성(correctness)**이었습니다. 속도 제한기(rate limiter)는 여러 게이트웨이 노드에 걸쳐 모든 요청마다 Redis에 접근합니다. Go나 Node였다면 레이스 컨디션(race condition)에 대해 불안했을 것입니다. 하지만 Rust에서는 컴파일러가 레이스 컨디션을 아예 컴파일하지 못하게 막습니다. 이는 상투적인 표현이 아닙니다. 소유권 모델(ownership model)이 올바른 설계를 강제했기 때문에, 비즈니스 로직 내에 단 하나의 Mutex도 없이 분산 속도 제한기를 구축할 수 있었다는 실제적인 의미입니다.
아키텍처 심층 분석 (Architecture Deep Dive)
요청 파이프라인 (The Request Pipeline)
모든 요청은 프로바이더(provider)에 도달하기 전 계층화된 미들웨어 스택을 통과합니다:
클라이언트 요청 (Client Request)
│
▼
...
5단계와 6단계가 흥미로운 부분입니다.
인증 (Authentication): 왜 Argon2id인가?
비밀번호 해싱의 업계 표준은 Argon2id입니다. 이는 Password Hashing Competition에서 우승한 데에는 이유가 있습니다. 하지만 API 키의 경우, 대부분의 사람들은 단순히 bcrypt를 사용하거나, 더 나쁜 경우에는 평문 SHA256 해시를 저장합니다. 여기서 제가 Argon2id를 사용한 이유는 다음과 같습니다:
- 메모리 하드(Memory-hard) — GPU 기반의 브루트 포스(brute force) 공격 비용이 높음
- 구성 가능한 병렬성(parallelism) 및 메모리 비용
- Rust 생태계의
argon2크레이트(crate)가 매우 훌륭함
키 생성 시에는 해시값만 저장됩니다. 모든 요청 시에는 제시된 키를 argon2::verify_encoded()로 검증합니다. 원본 키는 데이터베이스에 단 한 번도 저장되지 않습니다.
// 키 생성: 해싱 및 저장
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
...
속도 제한 (Rate Limiting): Redis 슬라이딩 윈도우 (Sliding Window)
고정 윈도우 (Fixed window) 속도 제한에는 당신을 괴롭힐 수 있는 예외 케이스가 있습니다. 윈도우 경계 직전과 직후에 요청이 몰리면 제한치의 2배에 달하는 버스트 (burst)가 발생할 수 있습니다. 슬라이딩 윈도우 (Sliding window) 방식은 이를 해결합니다.
구현 방식은 API 키당 하나의 Redis 정렬된 집합 (Sorted Set)을 사용합니다. 각 요청 시 다음 과정을 거칩니다:
- 윈도우 기간(현재 60초)보다 오래된 모든 항목을 제거합니다.
- 남아 있는 항목의 개수를 셉니다.
- 개수가 제한치(limit)보다 크거나 같으면(≥) → 429 에러와 함께 거부합니다.
- 그렇지 않으면 현재 타임스탬프를 추가하고 계속 진행합니다.
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_millis() as f64;
...
이 모든 과정은 요청당 원자적 (Atomically)으로 실행됩니다. 여러 게이트웨이 노드가 동일한 Redis를 공유하기 때문에, 속도 제한은 수평적 복제본 (Horizontal replicas) 전체에 걸쳐 **전역적으로 일관성 (Globally consistent)**을 유지합니다.
멀티 프로바이더 라우팅 (Multi-Provider Routing)
프로바이더 선택은 순수하게 모델 이름에 의해 결정되며, 요청마다 별도의 설정이 필요하지 않습니다:
pub fn select_provider(model: &str) -> Provider {
if model.starts_with("gpt-") || model.starts_with("o1") || model.starts_with("o3") {
Provider::OpenAI
...
이는 단일 엔드포인트에서 model 필드만 변경함으로써 gpt-4 대 claude-3-5-sonnet 대 llama3를 A/B 테스트할 수 있음을 의미합니다. 인프라 변경이나 재배포(Redeploy)가 필요 없습니다.
워크스페이스 아키텍처 (The Workspace Architecture)
이 프로젝트는 세 개의 크레이트 (Crates)로 구성된 Cargo 워크스페이스입니다:
crates/
├── gateway/ # 메인 바이너리 — 라우팅, 미들웨어, HTTP 서버
├── shared/ # 라이브러리 — 도메인 모델, 프로바이더 클라이언트, 에러
...
shared를 분리한 것은 의도적인 결정이었습니다. 프로바이더 클라이언트 (OpenAI, Anthropic, Ollama)는 웹 프레임워크 의존성 없이 순수하게 reqwest + serde로만 구성되어 있습니다. 이는 HTTP 서버를 구동하지 않고도 프로바이더 로직을 테스트할 수 있음을 의미합니다. 또한 대시보드에서 타입을 중복 정의하지 않고도 사용 통계 (Usage stats)를 가져올 수 있음을 의미합니다.
gateway 크레이트의 상태는 Axum의 Extension을 통해 전달되는 단일 AppState 구조체입니다:
#[derive(Clone)]
pub struct AppState {
pub db: PgPool,
...
Clone은 내부적으로 풀(pool)과 커넥션 매니저(connection manager)를 위해 Arc를 사용합니다. 이는 비동기 태스크(async tasks) 간에 저렴한 공유 소유권(shared ownership)을 제공하며, 요청당 할당(allocation)이 발생하지 않습니다.
관측성 (Observability): 실제로 무엇을 계측하는가
모든 프로덕션 서비스는 결국 다음과 같은 질문을 던지게 됩니다: "왜 그 요청이 느렸을까?" 여러분은 누군가를 깨우지 않고도 그 질문에 답할 수 있어야 합니다.
구조화된 로깅 (Structured logging) — 프로덕션 환경에서는 모든 요청이 JSON 로그 라인을 방출합니다:
{
"timestamp": "2026-05-31T10:30:00.000Z",
"level": "INFO",
...
해당 request_id는 첫 번째 미들웨어(middleware)에 의해 주입되며, 업스트림 프로바이더(upstream provider) 호출을 통해 전파됩니다. 문제가 발생하면 request_id로 grep 검색을 하여 로그 전체에 걸친 전체 트레이스(trace)를 확보할 수 있습니다.
Prometheus 메트릭 (Prometheus metrics) — AI 게이트웨이로서 제가 실제로 중요하게 생각하는 네 가지 메트릭입니다:
| 메트릭 | 중요성 |
|---|---|
gateway_requests_total{model,provider,status} | 모델별 에러율 |
| ... |
Grafana는 monitoring/grafana/provisioning/을 통해 사전 프로비저닝(pre-provisioned)되어 있습니다. docker-compose up 한 번이면 라이브 대시보드를 사용할 수 있습니다. 수동 데이터 소스(datasource) 설정이 필요 없습니다.
상태 프로브 (Health Probes): Liveness vs Readiness
이 구분은 Kubernetes 및 ECS에서 중요하며, 대부분의 튜토리얼이 이를 잘못 설명하곤 합니다.
GET /health/live — 프로세스가 실행 중인 한 200을 반환합니다. 외부 의존성(external deps)은 절대 확인하지 않습니다. 이는 오케스트레이터(orchestrator)가 태스크를 종료하고 재시작할지 여부를 결정할 때 사용합니다.
GET /health/ready — 데이터베이스에 접속 가능한 경우에만 200을 반환합니다. 이는 로드 밸런서(load balancer)가 트래픽을 보낼지 여부를 결정할 때 사용합니다. 만약 DB 커넥션 풀(connection pool)이 고갈되면, 이 프로브는 실패하며 LB는 해당 인스턴스로의 라우팅을 중단합니다.
// Readiness: 트래픽을 받기 전에 DB 확인
async fn health_ready(State(state): State<AppState>) -> impl IntoResponse {
match sqlx::query("SELECT 1").fetch_one(&state.db).await {
...
AWS 배포 (Terraform)
인프라는 infra/main.tf에 완전히 정의되어 있습니다:
Internet → ALB → ECS Fargate (2+ tasks) → RDS PostgreSQL
→ ElastiCache Redis
언급할 만한 몇 가지 결정 사항들:
EC2 대신 ECS Fargate 사용 — 인스턴스 관리나 용량 예약(capacity reservations)이 필요 없습니다. 게이트웨이는 상태가 없는(stateless) 구조이므로, 수평 확장(horizontal scaling)은 원하는 태스크(task) 수를 늘리는 것만으로 충분합니다. Rust 바이너리의 50ms 콜드 스타트(cold start) 덕분에 새로운 태스크가 빠르게 준비됩니다.
자체 관리형 Postgres 대신 RDS 사용 — 사용 로그(usage logs) 테이블은 계속 커질 것입니다. 감사 추적(audit trails)이 중요한 서비스에서 자동 백업, 특정 시점 복구(point-in-time recovery), 그리고 다중 AZ(Multi-AZ) 구성은 비용을 지불할 가치가 있습니다.
자체 관리형 대신 ElastiCache Redis 사용 — 속도 제한기(rate limiter)의 정확성은 Redis에 달려 있습니다. 자동 장애 조치(automatic failover) 기능이 있는 관리형 Redis를 사용하면, Redis가 재시작되더라도 모든 키에 대해 속도 제한 기능이 일시적으로 중단되지 않습니다.
테스트: 3계층 (The Three Layers)
테스트 스위트(test suite)는 명확한 피라미드 구조를 가집니다:
Unit tests — I/O 없음. 설정 파싱(Config parsing), 프로바이더 라우팅 로직,
요청/응답 직렬화(request/response serialization).
...
모의 테스트(mock tests)가 가장 가치 있었습니다. Anthropic 클라이언트가 Messages API에서 max_tokens 필드를 잘못된 위치에 보내는 버그를 잡아낼 수 있었습니다. 단위 테스트(Unit tests)로는 이를 잡아낼 수 없었을 것입니다. 실제 Anthropic API를 대상으로 하는 통합 테스트(Integration tests)는 느리고 불안정(flaky)했을 것입니다. Wiremock이 적절한 도구였습니다.
# 전체 스위트 실행
./scripts/test.sh all
...
성능 벤치마크 (Performance Benchmarks)
게이트웨이 오버헤드만 측정 (프로바이더 지연 시간 제외):
| 지표 (Metric) | 값 (Value) |
|---|---|
| 처리량 (Throughput) (health endpoint) | ~45,000 req/s |
| ... |
10,000개의 연결에서 45 MB를 유지한다는 점은 사람들을 계속 놀라게 하는 수치입니다. Node.js였다면 해당 동시성(concurrency)에서 500 MB를 훨씬 넘었을 것입니다. 이 차이는 알고리즘의 차이가 아닙니다. 연결당 스레드(thread-per-connection) 모델에서는 메가바이트 단위인 반면, Rust의 비동기 태스크(async tasks)는 태스크당 수백 바이트의 스택(stack)만 사용하기 때문입니다.
다음에 추가할 사항
이 프로젝트는 현재 프로덕션 환경에 투입될 준비가 되어 있지만, 몇 가지 사항이 로드맵에 있습니다:
스트리밍 지원 (Streaming support) — 현재 구현은 응답을 반환하기 전에 전체 응답을 버퍼링합니다. Claude/GPT의 긴 출력물의 경우, 클라이언트가 토큰이 도착하는 대로 볼 수 있도록 text/event-stream SSE (Server-Sent Events) 전달 방식이 필요합니다.
토큰 예산 강제 (Token budget enforcement) — 현재 속도 제한 (Rate limiting)은 요청 기반입니다. AI 게이트웨이에 더 유용한 기본 단위는 토큰 기반 예산입니다: "이 키는 하루에 100만 토큰을 사용할 수 있습니다."
폴백을 포함한 재시도 (Retry with fallback) — 만약 OpenAI가 503 오류를 반환하면, 자동으로 Ollama로 재시도합니다. 제공자 추상화 (Provider abstraction)는 이미 구현되어 있으므로, 재시도 정책 (Retry policy) 레이어만 추가하면 됩니다.
프롬프트 캐싱 메트릭 (Prompt caching metrics) — Anthropic과 OpenAI 모두 프롬프트 캐싱 (Prompt caching)을 지원합니다. 키별 캐시 히트율 (Cache hit rates)을 추적하면 팀에게 실행 가능한 비용 절감 데이터를 제공할 수 있습니다.
빠른 시작 (Quick Start)
git clone https://github.com/MihirMohapatra/rust-ai-gateway
cd rust-ai-gateway
cp .env.example .env
...
Grafana 대시보드 주소: http://localhost:3002 (admin/admin).
등록 및 API 키 발급:
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "secure-pass"}'
...
기존 OpenAI SDK를 이 게이트웨이로 연결하세요:
from openai import OpenAI
client = OpenAI(
...
마치며 (Closing Thoughts)
이 프로젝트를 구축하면서 인프라 소프트웨어에 있어 "프로덕션급 (Production-grade)"이 실제로 무엇을 의미하는지에 대해 진지하게 고민하게 되었습니다. 그것은 단순히 "작동한다"는 것만이 아닙니다. 다음과 같은 것들을 의미합니다:
- 관측 가능성 우선 (Observability first) — 장애가 발생한 후가 아니라, 발생하기 전에 무엇을 하고 있는지 알아야 합니다.
- 설계된 장애 모드 (Failure modes are designed, not discovered) — 우아한 종료 (Graceful shutdown), 서킷 브레이킹 (Circuit breaking), Liveness vs Readiness.
- 구조적인 보안 (Security is structural) — Argon2id는 단순한 세부 사항이 아니라, 방어 가능한 유일한 선택입니다.
- 벤치마크는 가설이다 (Benchmarks are hypotheses) — 45 MB라는 메모리 수치는 대안들과 비교했을 때만 의미가 있습니다.
AI 기반 제품을 구축하면서 "어떤 제공자를 사용해야 하고 어떻게 추적해야 하는가"라는 문제에 직면해 있다면, 이 글이 유용한 시작점이 되기를 바랍니다.
도움이 되었다면 저장소(repo)에 Star를 눌러주시고, 부족한 점을 발견하면 이슈(issue)를 생성해 주세요. PR(Pull Request)은 언제나 환영하며, 특히 스트리밍(streaming) 기능에 대한 기여를 기다립니다.
저는 FinTech(HSBC, Westpac, FedEx) 분야의 경력을 가진 시니어 백엔드 엔지니어(Senior Backend Engineer)이며, 현재 Rust와 Go를 사용하여 AI/ML 인프라를 구축하고 있습니다. 미국 또는 유럽 소재 기업의 시니어 백엔드 및 AI 인프라(AI infra) 직무에 열려 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기