1. 왜 ChatGPT App의 경계를 보호해야 하는가
전통적인 웹 애플리케이션에서 사용자는 브라우저이며, 비교적 예측 가능한 방식으로 엔드포인트를 두드립니다. ChatGPT Apps 세계에서는 새로운 유형의 클라이언트가 생깁니다: 언제 어떤 도구를 호출할지 스스로 결정하는 LLM.
모델은 다음을 할 수 있습니다:
- 하나의 대화에서 같은 tool을 연달아 여러 번 호출할 수 있음;
- 실험하기: “그럼 suggest_gifts를 매개변수를 조금 바꿔서 한 번 더 호출해 보면?”;
- 수백 명의 사용자에 대해 병렬로 동작.
여기에 봇, 테스트 스크립트, 자체 코드 오류(예: tool‑call을 계속 트리거하는 무한 루프)까지 더하면, 선의로 유발되는 DoS에 가까운 완벽한 레시피가 됩니다.
치명적인 문제는 비용입니다. 각 tool‑call은 다음을 할 수 있습니다:
- 외부 유료 API(배송, 결제, 카탈로그 등)를 호출하고,
- 다른 LLM을 호출하고(예: RAG 검색),
- 무거운 백그라운드 작업을 시작합니다.
제한과 경계 보호가 없다면, 한 명의 ‘운 나쁜’ 클라이언트만으로도 다음을 초래할 수 있습니다:
- 게이트웨이 뒤의 모든 백엔드 서비스를 다운시킴(Gift API, Commerce API 등),
- 외부 API의 할당량을 소진시키고,
- 모델 비용을 눈에 띄게 태워 버림.
이 강의의 목표는 gateway/proxy + rate limiting + 큐 + backpressure로 이러한 잠재적 재난을 통제 가능한 시스템으로 바꾸는 방법을 보여주는 것입니다.
Insight
ChatGPT 플랫폼은 외부 트래픽으로부터 여러분의 MCP 서버를 보호하는 어떠한 메커니즘도 제공하지 않습니다. MCP Jam 같은 유틸리티를 포함해, 인터넷의 어떤 클라이언트든 해당 서버로 요청을 보낼 수 있습니다.
ChatGPT가 제공할 수 있는 것은 역방향 프록시(예: NGINX)에 allowlist를 설정하여 IP 기반으로 인바운드 트래픽을 제한하는 정도입니다. IP 필터링이 설정되어 있지 않다면, MCP 서버는 완전히 개방된 상태로 남아 있어 안전하지 않습니다. 여러분에게도, 사용자들에게도 마찬가지입니다.
2. 백엔드 서비스와 에이전트 앞의 “방패”로서 Proxy/Gateway
이전에 봤던 그림을, 이제 보안 관점에서 다시 떠올려 봅시다.
전형적인 구성을 상상해 보세요:
flowchart LR
ChatGPT["ChatGPT / 위젯"]
--> GW["MCP Gateway (Auth, Rate Limit, Logs)"]
GW --> GiftAPI["Gift REST API (선물 추천)"]
GW --> CommerceAPI["Commerce REST API (checkout, ACP)"]
GW --> Analytics["Analytics Service / REST API"]
GW --> Queue["작업 큐"]
Queue --> Worker["Background workers"]
Gateway는 외부 세계(ChatGPT, 웹훅, 테스트 클라이언트)와 그 밖의 모든 것 사이에 있습니다. Gateway는 다음을 수행합니다:
- 모든 인바운드 요청을 완전히 관찰하고,
- 가장 먼저 토큰과 요청 포맷을 검사하며,
- 명백히 불가능한 것들(이상한 host, 특이한 path, 과도하게 큰 body)을 걸러내고,
- 어떤 내부 REST/HTTP 서비스로 요청을 보낼지 결정합니다.
이 레이어에서 다음도 함께 등장합니다:
- rate limiting — 일정 시간 동안 허용할 요청 수 제한;
- 기초적인 backpressure — 내부 서비스가 이미 과부하라면 거절;
- 비동기 전환 — 무거운 작업은 즉시 큐로, 클라이언트 응답은 “접수됨, 대기 중”.
즉, gateway는 단순한 “router”가 아니라 “방탄조끼”입니다. 다만, 이를 “모든 비즈니스의 모놀리식”으로 만들지 않는 것이 중요합니다. 이 점은 지난 강의에서 이미 이야기했습니다.
3. 어떤 트래픽 흐름을 제어해야 하는가
ChatGPT App 생태계에서는 제한과 보호 관점에서 특히 중요한 트래픽 유형이 보통 세 가지 있습니다.
첫째, ChatGPT에서 오는 MCP tool‑call입니다. suggest_gifts, get_product_details, create_checkout_session 등 MCP 프로토콜로 들어오는 모든 도구 호출이 여기에 해당합니다. 에이전트까지 함께 쓰면 모델은 이를 꽤 빠르게 생성할 수도 있습니다.
둘째, 우리 백엔드에서 외부 API로 나가는 요청입니다. 서비스 내부에서는 카탈로그, 물류, 결제 등 외부 시스템에 대해 자체 rate limit을 둘 수 있습니다. 이를 위반하면 차단, 패널티, 품질 저하가 발생할 수 있습니다.
셋째, 인바운드 웹훅입니다. ACP, 결제 제공업체(Stripe 등), 배송사가 보내는 알림으로, 사용자 활동과 무관하게 도착합니다. 우리 endpoint가 느리거나 오류를 반환하면 외부 시스템은 재시도(retries)를 시작하고, 반복 알림의 “스톰”을 일으킬 수 있습니다.
GiftGenius에서는 대략 다음과 같습니다:
- 사용자와 모델이 suggest_gifts, find_similar_gifts를 활발히 호출;
- checkout 도구가 ACP/커머스 백엔드를 호출;
- 결제 후 결제사가 payment.succeeded / payment.failed 웹훅을 보냄.
이 모든 흐름이 하나의 지점 — Gateway에 모입니다. 그러므로 바로 그 지점에 “카운터, 필터, 신호등”을 두는 것이 합리적입니다.
4. Rate limiting: 기본 방어와 비용 절감
우리 컨텍스트에서 rate limiting이란
Rate limiting은 특정 클라이언트가 단위 시간 동안 보낼 수 있는 요청 수를 제한하는 메커니즘입니다. 인터넷만큼 오래된 아이디어지만, ChatGPT Apps 문맥에서는 다음 세 가지를 즉시 해결합니다:
- 단일 클라이언트(또는 버그)가 여러분의 서비스를 다운시키는 것을 방지;
- 외부 API의 한도를 지키도록 도움;
- 모델 호출 비용이 통제 불능으로 치솟는 것을 방지.
고전적인 알고리즘은 다음과 같습니다:
- 고정 윈도우(Fixed Window),
- 슬라이딩 윈도우(Sliding Window),
- 토큰 버킷(Token Bucket).
핵심은 개념 이해입니다. “1분 동안 N개 이하”, “요청 하나는 토큰 하나를 소모, 토큰은 초당 X개로 보충” 등. 실제 구현은 보통 라이브러리나 API Gateway가 맡습니다.
어디에 제한을 둘 것인가
제한은 여러 레벨에서 둘 수 있습니다.
역방향 프록시(Nginx, Cloudflare, AWS API Gateway) 레벨에서는 다음이 유용합니다:
- IP 기준의 가장 거친 트래픽 차단,
- 요청 본문 크기 제한,
- 단순한 DDoS 패턴 방어.
MCP Gateway(애플리케이션) 레벨에서는 더 “의미 있는” rate limiting을 합니다:
- 사용자 기준(userId에서 추출),
- 조직 기준(tenantId),
- 작업 유형 기준(예: create_checkout_session은 강하게, search는 상대적으로 느슨하게),
- 소스 기준(webhook vs tool‑call).
특히 비용이 큰 연산에 대해서는 마이크로서비스 내부에 별도 제한을 추가할 수도 있지만, 이는 다음 단계의 상세화입니다.
제한의 키를 어떻게 선택할 것인가
가장 흔한 실수는 IP 주소 기준으로만 제한하는 것입니다. ChatGPT의 경우 거의 의미가 없습니다:
- 모든 요청이 OpenAI의 동일한 IP 대역에서 올 수 있고,
- 서로 다른 사용자가 동일한 IP 뒤에 있을 수 있습니다.
더 유용한 키는 다음과 같습니다:
- userId — 여러분 앱의 특정 사용자,
- tenantId — 조직(B2B에서 하나의 채팅을 여러 직원이 같이 쓰는 경우),
- API 토큰 또는 clientId — 통합이 여러 개인 경우.
GiftGenius에서는 보통 ChatGPT가 MCP 호출에 전달하는 토큰에서 추출한 userId + tenantId면 충분합니다.
TypeScript로 구현하는 간단한 rate limiting
작은 Express 기반 MCP Gateway가 있다고 가정해 봅시다. 여기에 가장 단순한 rate limiting을 추가합니다: 사용자당 분당 tool‑call 30회 이하.
// 원시적인 rate limiting: userId당 분당 N개의 요청
const WINDOW_MS = 60_000;
const MAX = 30;
const hits = new Map<string, { ts: number; count: number }>();
function rateLimit(req: Request, res: Response, next: NextFunction) {
const userId = (req.headers["x-user-id"] as string) ?? "anonymous";
const now = Date.now();
const rec = hits.get(userId) ?? { ts: now, count: 0 };
if (now - rec.ts > WINDOW_MS) { // 윈도우 만료 — 처음부터 다시 시작
rec.ts = now;
rec.count = 0;
}
rec.count += 1;
hits.set(userId, rec);
if (rec.count > MAX) {
return res.status(429).json({
error: "rate_limit_exceeded",
retryAfterSec: 60,
message: "Too many tool calls, please retry later."
});
}
next();
}
이제 MCP 라우트에 적용합니다:
// 모든 MCP tool-call에 middleware 적용
app.post("/mcp/tools/call", rateLimit, async (req, res) => {
const result = await callBackendForTool(req.body); // Gift/Commerce/Analytics API에 대한 REST 호출
res.json(result);
});
핵심 포인트:
- 의미 있는 오류(error: "rate_limit_exceeded")를 반환하고, 단순한 500만 보내지 않습니다;
- 모델은 이 오류를 읽고 상황을 이해해 사용자에게 제대로 설명할 수 있으며, 근거 없이 헛소리를 하지 않습니다.
실제 프로덕션에서는 카운터가 단일 프로세스 메모리가 아니라 Redis 같은 공용 저장소에 있어 클러스터에서 동작해야 합니다. 원리를 이해하는 데에는 이 정도면 충분합니다.
Rate limiting과 게이트웨이 레벨 제한은 요청 폭주로부터 보호하지만, 또 다른 문제 — 개별 연산이 매우 무겁고 오래 걸릴 수 있음 — 은 해결하지 못합니다. 여기서는 동기 HTTP만으로 부족하며, 큐와 비동기 작업이 필요합니다.
5. 큐와 비동기 작업: 동기로는 더 이상 어려울 때
ChatGPT 타임아웃 문제
레이트 리미팅을 잘 설정했더라도, ChatGPT(및 일반 HTTP 클라이언트)는 응답이 너무 느린 것을 싫어합니다. 플랫폼은 tool‑call 실행 시간을 제한합니다. “아주 강력한 추천 알고리즘” 같은 것이 끝날 때까지 기다리면 다음이 발생합니다:
- 사용자는 끝없는 스피너를 보게 되고,
- 플랫폼은 타임아웃으로 요청을 끊고,
- 모델은 “문제가 생겼다”고 판단해 지어내기 시작합니다.
해결책: 무거운 연산을 비동기 모드로 전환합니다. 전형적인 패턴은 다음과 같습니다:
- Gateway가 요청을 받는다.
- 작업을 큐에 넣는다.
- 202 Accepted와 jobId를 즉시 반환한다.
- 별도의 워커가 큐에서 작업을 가져와 처리한다.
- 클라이언트(우리 위젯 또는 추가 tool을 통해 ChatGPT)가 jobId로 상태를 주기적으로 조회하거나 MCP 이벤트로 알림을 받는다.
ChatGPT App 관점에서는 보통 두 개의 도구로 보입니다. 첫 번째 tool이 요청을 받아 큐에 넣고 jobId를 반환하며, 두 번째 tool이 해당 jobId로 상태를 조회하고 결과를 수집합니다. 진행 상황 이벤트는 MCP 알림으로도 보낼 수 있습니다.
GiftGenius용 미니 큐(코드 예시)
수십 초가 걸릴 수 있는 무거운 도구 generate_large_gift_report가 있다고 해 봅시다. 실제 App이라면 jobId만 반환하고, 별도 tool get_report_status로 모델이나 위젯이 상태와 결과를 가져오게 할 수 있습니다. Gateway 레벨에서 이를 위한 전용 endpoint와 큐를 만듭니다.
type Job = { id: string; payload: any };
const queue: Job[] = [];
const MAX_QUEUE = 100;
app.post("/mcp/tools/generate_report", (req, res) => {
if (queue.length >= MAX_QUEUE) {
return res.status(503).json({
error: "system_busy",
message: "System is busy, please retry later."
});
}
const job: Job = { id: crypto.randomUUID(), payload: req.body };
queue.push(job);
res.status(202).json({ jobId: job.id, status: "accepted" });
});
그리고 200ms마다 하나의 작업을 가져가는 아주 단순한 워커:
async function processJob(job: Job) {
// 여기서 실제 백엔드 서비스나 에이전트 워크플로우를 REST로 호출
await handleHeavyGiftReport(job.payload);
}
setInterval(async () => {
const job = queue.shift();
if (!job) return;
await processJob(job);
}, 200);
물론 이는 크게 단순화된 예시입니다:
- 실서비스에서는 큐가 Redis, SQS, Kafka 등에 존재하고,
- 작업 상태를 별도로 저장하여 조회할 수 있어야 하며,
- 워커는 보통 여러 개입니다.
하지만 개념은 분명합니다: Gateway는 모든 처리가 끝날 때까지 요청을 열어 두지 않습니다. 요청을 받고, 작업에 넣고, 빠르게 응답합니다.
6. Backpressure: 자신의 큐에 떠밀리지 않기
backpressure와 rate limiting의 차이
Rate limiting은 주로 “단일 클라이언트가 단위 시간에 몇 개의 요청을 할 수 있는가?”에 답합니다. 특정 클라이언트의 과도한 활동이나 버그로부터의 보호입니다.
반면 backpressure는 “시스템이 동시에 무너지지 않고 처리할 수 있는 작업/요청의 총량은 얼마인가?”를 묻습니다. 이것은 누가 보냈는지와 무관한 전체 부하의 문제입니다.
예:
- rate limiting: “사용자는 suggest_gifts를 분당 30회보다 자주 호출할 수 없다.”
- backpressure: “대기 중인 작업이 100개를 넘으면 새 요청을 모두 거절하기 시작한다.”
이상적으로 두 메커니즘은 서로 보완합니다. rate limit이 클라이언트를 통제하고, backpressure는 많은 사용자가 한꺼번에 몰려도 시스템을 지켜 줍니다.
활성 작업 제한의 간단한 구현
가장 간단한 backpressure는 내부의 활성 호출 수를 제한하는 것입니다. 예: 특정 백엔드/REST 서비스(Gift API, Commerce API 등)에 동시에 50개보다 많은 활성 tool‑call을 유지하지 않기.
let activeCalls = 0;
const MAX_ACTIVE = 50;
app.post("/mcp/tools/call", async (req, res) => {
if (activeCalls >= MAX_ACTIVE) {
return res.status(429).json({
error: "gateway_overloaded",
message: "Gateway is temporarily overloaded, please retry later."
});
}
activeCalls += 1;
try {
const result = await callBackendForTool(req.body); // Gift/Commerce/Analytics API에 대한 REST 호출
res.json(result);
} catch (err) {
console.error("Tool call error", err);
res.status(500).json({ error: "internal_error" });
} finally {
activeCalls -= 1;
}
});
여기서 일어나는 일:
- 동시에 실행 중인 요청 수가 MAX_ACTIVE보다 작을 때만 새 호출을 통과시킵니다;
- 한도를 초과하면 즉시 의미 있는 오류로 응답합니다;
- 오류가 나도 “슬롯”을 잃지 않도록 finally에서 반드시 카운터를 감소시킵니다.
이것이 가장 단순한 backpressure입니다: 무턱대고 다 받다가 죽는 대신 “지금은 불가, 나중에 다시 시도해 달라”고 정직하게 말합니다.
추후에는 다음을 고려할 수 있습니다:
- 작업 유형별로 서로 다른 MAX_ACTIVE 값(예: checkout은 거의 항상 통과, 보고서 생성은 더 엄격히 제한),
- 부하 지표에 따라 동적으로 한도를 조정.
7. 웹훅과 “스톰”: 인바운드 이벤트 보호
지금까지는 주로 우리가(또는 ChatGPT가) 시작하는 요청(tool‑call, 아웃바운드 요청, 비동기 작업)에 집중했습니다. 하지만 실제로 Gateway 부하의 중요한 원천이 하나 더 있습니다 — 외부 시스템에서 들어오는 인바운드 웹훅입니다.
웹훅은 동전의 반대면입니다. tool‑call은 우리가(모델을 통해) 시작하지만, 웹훅은 외부 서비스가 시작합니다. 이는 4장에서 말한 세 번째 트래픽 유형으로, 시간과 빈도를 우리가 통제할 수 없지만, 다운 없이 소화해야 합니다. 결제, ACP, 물류 — 모두 의미 있는 변경이 있을 때마다 우리 endpoint로 알림(웹훅)을 보냅니다: “결제 완료”, “주문 생성”, “배송 상태 갱신”.
문제는 다음과 같은 경우에 시작됩니다:
- 우리 endpoint가 느리게 응답하거나,
- 오류로 응답하거나,
- 간헐적으로 사용 불가능한 경우.
그러면 외부 서비스는 모범 사례에 따라 재시도(retries)를 수행합니다. 운이 나쁘면 웹훅 “스톰” — 수십, 수백 번의 반복 이벤트 — 을 맞게 됩니다.
이런 과한 배려에 쓰러지지 않으려면, Gateway 레벨에서 다음을 수행해야 합니다:
- 소스별 인바운드 웹훅 제한: 예를 들어 특정 제공업체의 동일 event_type에 대해 1분에 10건 이하.
- JSON 파싱 이전에 서명 검증: HMAC 서명 등으로 위조 요청을 차단.
- 이벤트 처리를 멱등하게: event_id 등으로 중복 이벤트가 주문/결제 중복으로 이어지지 않게.
- 강한 스톰 시 추가 backpressure: 다운스트림 서비스가 못 따라가면 일시적으로 “503: 나중에 다시”를 응답.
가장 단순한 예(아이디어 차원, 프로덕션 코드는 아님):
app.post("/webhooks/stripe", rateLimitWebhook, (req, res) => {
const sig = req.headers["stripe-signature"] as string;
if (!isValidSignature(req.rawBody, sig)) {
return res.status(400).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
if (isAlreadyProcessed(event.id)) {
return res.json({ received: true }); // 멱등성
}
handleStripeEvent(event);
res.json({ received: true });
});
여기서 Gateway 레벨에서 우리는 다음을 합니다:
- 웹훅에 대해 별도의 rate limiting 정책을 적용하고,
- 본문을 신뢰하기 전에 서명을 검증하며,
- isAlreadyProcessed로 중복을 방지합니다.
8. GiftGenius에 적용: 제한과 큐 정책 예시
이제 추상에서 벗어나, 학습용 GiftGenius에서는 어떻게 보일지 살펴봅시다.
세 가지 핵심 시나리오를 가정합니다:
- 선물 검색(suggest_gifts, find_similar_gifts).
- 주문 생성/checkout(create_checkout_session, confirm_order).
- 웹훅 수신 — 결제 제공업체와 ACP로부터.
각 시나리오마다 다음을 정하는 것이 합리적입니다:
- 어떤 키로 제한을 셀 것인지,
- 분당 몇 건을 허용할지,
- 초과 시 어떻게 동작할지.
예를 들어:
| 시나리오 | 제한 키 | 분당 한도 | 초과 시 동작 |
|---|---|---|---|
| 선물 검색 | userId | 30 | 429 + “검색 조건을 좁히세요” 안내 |
| 주문 생성 | userId + tenantId | 5 | 429 + “시도가 너무 많습니다. 주문을 확인하세요” |
| 인바운드 웹훅 | provider + eventType | 10 | 429/503, 로깅, 필요 시 서비스 디그레이드 |
웹훅의 경우 보통 “제공업체 + 이벤트 유형” 조합으로 제한하는 것이 더 합리적이며, event_id 기반의 별도 멱등성 메커니즘으로 중복을 걸러냅니다.
코드에서는 이를 서로 다른 미들웨어로 분리합니다: rateLimitSearch, rateLimitCheckout, rateLimitWebhook.
“1년치 선물에 대한 큰 PDF 보고서 생성” 같은 무거운 작업에는 앞서 보여준 큐+비동기 패턴을 사용합니다. 이때 Gateway는:
- ChatGPT로부터 요청을 받고,
- 작업을 큐에 넣고,
- jobId와 상태 조회 방법을 모델에 전달하며,
- 큐 크기를 제한(backpressure)하여 시스템이 넘치지 않도록 합니다.
중요 포인트: rate limiting과 backpressure는 보안과 안정성만이 아니라 UX의 문제이기도 합니다. 타임아웃까지 스피너만 보거나 “Internal Server Error”를 보게 하는 것보다, “지금은 혼잡하니 1분 뒤에 다시 시도하자”는 안내가 훨씬 좋습니다.
9. 미니 실습: 우리 MCP Gateway에 보호 장치 추가
이론으로 그치지 않도록, 학습 프로젝트에서 바로 적용할 수 있는 미니 실습을 구성해 봅시다.
모든 MCP tool‑call에 대한 rate limiting
위에서 본 rateLimit 미들웨어를 추가하고 /mcp/tools/call에 연결하세요. 우선 간단히 사용자당 분당 30회로 시작해 보세요. 그다음 다음을 실험합니다:
- 한도를 줄이고 App과 모델이 어떻게 반응하는지 확인;
- toolName을 미들웨어에 전달해 도구 유형별로 서로 다른 한도를 적용.
활성 호출 수 기반의 가장 단순한 backpressure
activeCalls 카운터와 MAX_ACTIVE 제한을 추가하세요. (여러 요청을 한 번에 보내는 스크립트 등으로) 부하를 흉내 내 보고, 어느 시점부터 Gateway가 gateway_overloaded로 응답하는지 확인해 보세요.
여기서 중요한 것은 동작 방식입니다. 모든 것이 무너지기를 기다리지 않고, 새 작업을 받지 않으면서 지금은 너무 뜨겁다고 정직하게 말합니다.
무거운 도구용 큐
하나의 무거운 연산을 선택(또는 setTimeout/긴 fetch를 넣어 인위적으로 “무겁게” 만들기)하고, “큐 + jobId” 패턴으로 바꾸세요. 최소한 다음이 필요합니다:
- endpoint POST /mcp/tools/generate_report — 작업을 큐에 넣고 jobId 반환;
- endpoint GET /jobs/:id — 상태(pending, done, error 및 필요 시 결과) 반환;
- processJob을 X ms마다 호출하는 워커.
이 정도면 BullMQ나 다른 큐 엔진과의 실제 통합이 어떤 모습일지 감을 잡기에 충분합니다.
10. 경계 보호에서 자주 발생하는 실수
오류 1: IP 기준으로만 제한하기.
ChatGPT Apps 세계에서는 거의 쓸모가 없습니다. 대부분의 요청이 OpenAI 주소에서 오며, 모든 사용자가 같은 IP 뒤에 있게 됩니다. 결국 누군가가 모두의 한도를 태우지만, 진짜 원인은 알 수 없습니다. userId, tenantId 또는 토큰으로 제한하고, IP는 역방향 프록시에서 아주 거친 필터로만 사용하세요.
오류 2: 의미 있는 오류 대신 맹목적인 500을 반환하기.
한도 초과나 과부하 시 500 Internal Server Error만 보내면, 모델은 아무것도 이해하지 못하고 지어내기 시작합니다. 반면 구조화된 오류 코드(rate_limit_exceeded, gateway_overloaded)와 사람 친화적 설명을 제공하면 LLM이 상황을 올바르게 설명하고, 필요 시 나중에 재시도할 수 있습니다.
오류 3: backpressure 없는 무한 큐 만들기.
“그냥 다 큐에 넣자, 나중에 처리하자”가 유혹적입니다. 현실에서는 큐가 수천 개로 불어나고, 지연이 커지며, 메모리가 바닥나고, 사용자는 결과를 보지 못합니다. 큐 크기와 활성 연산 수는 항상 제한하세요. 503이나 429로 새 요청을 정직하게 거절하는 것이, 큐를 블랙홀로 만드는 것보다 낫습니다.
오류 4: rate limiting만 믿고 웹훅을 무시하기.
많은 팀이 ChatGPT로부터의 인바운드 트래픽만 보호하고, 웹훅은 “어찌 되겠지”로 둡니다. 결제 제공업체가 재시도를 시작하면, 진짜 스톰을 일으키는 것은 바로 웹훅일 수 있습니다. 웹훅 endpoint에는 별도의 한도, 서명 검증, 멱등 처리(아이템포턴시)가 필요합니다. 그렇지 않으면 동일 주문이 열 번씩 생성되기 쉽습니다.
오류 5: 모든 카운터와 큐를 단일 인스턴스 메모리에만 저장하기.
학습 프로젝트에는 괜찮지만, 프로덕션에서 Gateway를 여러 인스턴스로 확장하면 각 노드의 카운터가 따로 놀고, 한도가 전역으로 적용되지 않으며, 노드를 재시작하면 큐가 초기화됩니다. 실제 시스템에서는 제한 상태와 큐를 공용 저장소(예: Redis, 클라우드 큐 등)에 저장합니다. 확장과 프로덕션 강의에서 더 이야기하겠습니다.
오류 6: “어차피 중간자니까” 비즈니스 로직을 Gateway에 밀어 넣기.
“요청이 어차피 Gateway로 들어오니, 선물 추천 로직도 여기서 해 버리자”라는 유혹이 있습니다. 그러면 게이트웨이는 라우터이자 비즈니스 두뇌이자 로거인 모놀리스로 변합니다. 이는 확장성과 유지 보수를 크게 복잡하게 만듭니다. Gateway는 네트워크/인프라 레이어로 남아야 합니다. 인증, 인가, 한도, 캐시, 라우팅 — 예. 선물 추천 — 아니오.
오류 7: “우리는 작으니 상관없다”라고 생각하기.
“사용자가 백만 명도 아닌데 gateway/한도 없이도 되겠지”라고 자주 생각합니다. 실제로는 클라이언트 코드(또는 모델이 tool을 무한히 호출하도록 만드는 프롬프트) 한 번의 버그만으로도 작지만 치명적인 아포칼립스를 맞을 수 있습니다. 기본적인 rate limiting과 최소한의 backpressure는 사치가 아니라 프로덕션의 칫솔과 같습니다. 아프기 시작하기 전에, 처음부터 사용해야 합니다.
GO TO FULL VERSION