본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 25. 04:45

앱에서 매번 재구축할 필요 없이 사용자별 AI 비용 제한을 구현하는 방법

요약

OpenAI나 Anthropic API를 사용하는 앱에서 사용자별 AI 비용을 효율적으로 제한하는 아키텍처를 제안합니다. 기존의 조직 단위 한도나 DB 기반 방식의 한계를 극복하기 위해 Redis와 Edge Function을 활용한 저지연 솔루션을 설명합니다.

핵심 포인트

  • 조직 단위 지출 한도는 개별 사용자별 비용 통제가 불가능함
  • 단순 요청 횟수 제한은 토큰 사용량에 따른 비용 차이를 반영하지 못함
  • Postgres 기반의 기록 방식은 데이터베이스 읽기 지연(latency) 문제를 유발함
  • Redis를 활용해 마이크로초 단위로 지출액을 관리하고 동시성을 보장함
  • Cloudflare Worker와 같은 Edge Function을 사용하여 확인 로직의 지연을 최소화함

OpenAI 또는 Anthropic을 기반으로 앱을 구축하고 있다면, 아마 불편한 점을 하나 발견했을 것입니다. 바로 개별 사용자가 귀하에게 얼마나 많은 비용을 발생시킬 수 있는지 제한하는 네이티브(native)한 방법이 없다는 점입니다.

OpenAI는 조직(org) 수준의 지출 한도(spend caps)와 프로젝트 예산을 제공합니다. 이는 귀하와 OpenAI 사이의 결제 관계를 보호합니다. 하지만 새벽 3시에 귀하의 AI 기능을 루프(loop)로 실행하여 귀하가 깨어나기 전에 400달러의 API 비용을 발생시킬 수 있다는 사실을 알아낸 단 한 명의 사용도로부터 귀하를 보호해주지는 못합니다.

저는 이를 두 번이나 아주 힘들게 배웠습니다.

명백한 해결책들이 작동하지 않는 이유

대부분의 개발자가 가장 먼저 시도하는 것은 조직 수준의 지출 한도(spend cap)입니다. 월 100달러로 설정하고 끝내는 것이죠. 문제는 이 한도가 조직 전체에 적용된다는 점입니다. 헤비 유저 한 명이 이를 전부 소진해버리면 다른 모든 사용자는 아무것도 사용할 수 없게 됩니다. 또한 누가 그 비용을 썼는지에 대한 가시성(visibility)도 없습니다.

사람들이 두 번째로 시도하는 것은 요청 기반의 속도 제한(rate limiting)입니다. 각 사용자를 하루 100개의 요청으로 제한하는 것이죠. 이는 합리적으로 들리지만, gpt-4o-mini의 요청 100개는 비용이 거의 들지 않는 반면, 긴 컨텍스트 윈도우(context windows)를 가진 gpt-4o의 요청 100개는 50달러가 들 수도 있다는 사실을 깨닫기 전까지만 그렇습니다. 요청 횟수 계산과 비용은 완전히 다른 문제입니다.

사람들이 세 번째로 시도하는 것, 그리고 제가 처음 시도했던 방식은 기존 스택(stack)에 직접 구축하는 것입니다. 각 호출 후에 Postgres에 토큰 수를 기록하고, 다음 호출 전에 총합을 확인하는 방식입니다. 문제는 지연 시간(latency)입니다. 서버리스(serverless) 환경에서 AI 호출 자체에 더해 매번 AI 호출 전에 데이터베이스 읽기(database read)를 수행하면 모든 요청에 200-400ms가 추가됩니다. 사용자는 이를 즉시 알아차립니다.

실제로 작동하는 아키텍처

두 번의 실패 끝에 저는 깔끔하게 작동하는 방식을 찾아냈습니다.

카운터를 위한 Redis

Redis는 마이크로초 단위로 빠르고 동시 증가(concurrent increments)를 안전하게 처리하기 때문에 지출 카운터(spend counters)를 위한 적절한 도구입니다. 동일한 사용자의 두 요청이 동시에 도착하더라도 Redis는 중복 계산 없이 이를 처리합니다. Postgres는 이 지연 시간 측면에서 도저히 경쟁할 수 없습니다.

핵심 구조는 간단합니다:

spend:{user_id}:{YYYY-MM}  →  decimal (달러 단위의 누적 지출액)
blocked:{user_id}           →  "1" 또는 "0"

월별 키는 자연스럽게 초기화됩니다. 매달 새로운 키에 데이터를 쓰기 시작하면 이전 키는 만료됩니다. 별도의 cron job (예약 작업)이 필요하지 않습니다.

가로채기를 위한 Edge Function

모든 AI 호출 전에 수행되는 확인 작업은 가능한 한 빨라야 합니다. 이를 Cloudflare Worker에서 실행하면 Edge (엣지)에서 실행되므로, 앱 서버가 어디에 있든 물리적으로 가까운 곳에서 처리됩니다. 동일한 리전(region)에 있는 Cloudflare Worker에서의 Redis 읽기는 일반적으로 20ms 미만 내에 완료됩니다.

가로채기(intercept) 로직은 간단합니다:

  1. Redis에서 사용자의 현재 지출액을 읽습니다.
  2. 사용자의 티어(tier) 제한과 비교합니다.
  3. 허용(allow) 또는 차단(block)을 반환합니다.

허용 경로(allow path)에서는 데이터베이스 호출이 발생하지 않습니다. 허용 경로는 순수하게 Redis로만 처리됩니다.

Fire-and-forget 로깅

AI 호출이 완료된 후에는 비용을 기록하고 카운터를 업데이트해야 합니다. 이 작업은 사용자에게 전달되는 응답을 절대 방해해서는 안 됩니다. Cloudflare Worker의 waitUntil을 사용하여 응답을 이미 반환한 후에 로그 호출을 실행하십시오.

로그 호출이 더 무거운 작업을 수행합니다: 토큰 수(token counts)로부터 비용을 계산하고, INCRBYFLOAT를 사용하여 Redis 카운터를 증가시키며, 새로운 합계가 제한을 초과하면 차단(blocked) 키를 설정하고, 분석을 위해 이벤트를 데이터베이스에 기록합니다.

SDK 래퍼 (Wrapper)

가장 깔끔한 통합 방식은 AI 호출을 하나의 래핑(wrap) 함수로 감싸는 것입니다:

const callAI = guard(openai.chat.completions.create.bind(openai.chat.completions))

// 코드베이스의 다른 모든 부분은 완전히 변경되지 않습니다:
...

호출 전에는 가로채고, 호출 후에는 로그를 남깁니다. 만약 사용자가 차단되었다면, 업그레이드 메시지가 포함된 타입화된 에러(typed error)를 던집니다. 코드베이스의 나머지 부분은 전혀 변경할 필요가 없습니다.

스트리밍 (Streaming) 처리

대부분의 현대적인 AI 통합 방식은 타이핑 효과를 위해 스트리밍(streaming)을 사용합니다. 이는 스트림이 끝날 때까지 최종 토큰 수를 알 수 없기 때문에 비용 추적을 복잡하게 만듭니다.

OpenAI의 경우, 요청 시 stream_options: { include_usage: true }를 설정하는 것이 해결책입니다. 이렇게 하면 OpenAI가 전체 토큰 사용량(token usage)이 포함된 마지막 청크(chunk)를 추가합니다. Anthropic의 경우는 약간 더 복잡합니다. message_start 이벤트에는 입력 토큰(input tokens)이 포함되어 있고, message_delta에는 출력 토큰(output tokens)이 포함되어 있으며, 이들은 청크 객체 내의 서로 다른 경로에 위치합니다. 단순히 마지막 이벤트만 읽는 것이 아니라, 두 이벤트 모두에서 값을 누적해야 합니다:

const accumulated: Record<string, number> = {}
for await (const chunk of stream) {
  if (chunk.type === 'message_start' && chunk.message?.usage) {
...

한도를 초과하는 사용자는 어떻게 하나요?

가장 깔끔한 접근 방식은 한 번의 호출이 한도를 초과하는 것을 허용하고, 그다음 호출을 차단하는 것입니다.

스트리밍 호출이 완료되기 전에는 최종 비용을 알 수 없기 때문에 호출 전에 차단할 수는 없습니다. 스트리밍 중간에 차단하는 것 또한 사용자 경험(user experience) 측면에서 매우 좋지 않습니다. 따라서 흐름은 다음과 같습니다: 호출 전에 사용자가 이미 차단되었는지 확인하고, 호출 후에 새로운 총합이 한도를 초과하는지 확인합니다. 한도의 99%를 사용 중인 사용자는 차단되기 전까지 한 번의 호출을 더 수행할 수도 있습니다. 이는 괜찮습니다. 모바일 데이터 요금제나 은행이 작동하는 방식과 같습니다. 이를 명확하게 문서화하면 사용자들이 이해할 것입니다.

직접 구축하기

이 기능을 직접 구축하고 싶다면 다음 요소들이 필요합니다:

  • Upstash Redis (서버리스(serverless), 관대한 무료 티어, Cloudflare Workers에서 작동)
  • 인터셉트(intercept) 엔드포인트를 위한 Cloudflare Workers
  • 이벤트 로그 및 분석을 위한 기존 데이터베이스
  • AI 호출을 래핑(wrap)하는 npm 패키지

Worker 자체는 대략 150줄의 TypeScript로 작성됩니다. SDK 래퍼는 추가로 100줄 정도입니다. 까다로운 부분은 스트리밍 누적(streaming accumulation)을 정확하게 처리하는 것과, 로깅(logging)이 상위 단계에서 단순히 대기(await)되는 것이 아니라 진정한 의미의 '발사 후 망각(fire-and-forget)' 방식으로 이루어지도록 보장하는 것입니다.

지름길

저는 이 모든 것을 Nasca라는 독립적인 도구로 추출했습니다. 가입한 후, 토큰 계산 없이 달러 단위로 사용자 등급과 월간 지출 예산을 정의하고 통합 코드를 복사하여 붙여넣기만 하면 됩니다. SDK는 귀하의 AI 함수를 한 번 감싸며(wrap), 추적(tracking), 차단(blocking), 업그레이드 프롬프트(upgrade prompts), 그리고 사용자별 지출을 보여주는 대시보드 등 나머지 모든 것을 처리합니다.

OpenAI와 Anthropic을 지원합니다. 최대 250명의 최종 사용자(end-users)까지 무료로 사용할 수 있습니다.

nasca.dev

직접 구축하고 싶다면 이 포스트의 모든 내용이 시작하기에 충분할 것입니다. 만약 실제 제품을 출시하는 단계로 바로 넘어가고 싶다면, Nasca는 단 10줄의 코드로 이를 수행합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0