CodeGym /행동 /ChatGPT Apps /시스템 탄력성: timeouts, circuit breakers, bulkheads, 웹훅 스톰 방어

시스템 탄력성: timeouts, circuit breakers, bulkheads, 웹훅 스톰 방어

ChatGPT Apps
레벨 16 , 레슨 2
사용 가능

1. 왜 ChatGPT App에서 “탄력성”을 고민해야 하나

일반 웹 애플리케이션에서는 사용자가 URL과 브라우저 스피너를 보고 페이지를 새로고침할 수 있습니다. ChatGPT에서는 사용자가 하나의 화면만 봅니다: 채팅과 여러분의 App. 무언가 느려지면 누가 문제인지—OpenAI, 여러분의 Gateway, 결제 시스템, 혹은 옆 팀의 분석 마이크로서비스—사용자는 구분하지 못합니다. 사용자에게는 그저 “ChatGPT + 여러분의 App”일 뿐입니다.

tool-call3060초 동안 걸려 있으면, 모델은 계속 기다립니다… 최선의 경우 지연에 대해 사과합니다. 최악의 경우에는 여러분의 백엔드 데이터 대신 환각(hallucination)으로 답을 만들어냅니다. 그래서 탄력성은 SRE와 uptime만의 문제가 아니라, 응답 품질, 모델의 톤, Store 지표와도 직결됩니다.

ChatGPT App 생태계에는 서로 독립적인 여러 경로가 있습니다:

  • ChatGPT ↔ MCP Gateway.
  • Gateway ↔ 여러분의 backend-/REST 서비스(Gift REST API, Commerce REST API, Analytics Service 등).
  • 여러분의 서비스 ↔ 외부 API(LLM, 결제, 카탈로그).
  • 수신 웹훅(ACP, Stripe, 기타 통합) ↔ 여러분의 핸들러.

문제는 한 곳의 장애가 연쇄적으로 번질 수 있다는 점입니다: Gateway는 멈춘 서비스를 끝까지 기다리고, 워커는 막히고, 커넥션은 고갈되고, 클라이언트는 재시도를 시작합니다. 몇 분 만에 전형적인 “헬 모드”가 됩니다: 여기저기서 동시에 불나고 가라앉습니다. 오늘 이야기하는 네 가지 패턴이 바로 이런 상황에서 우리를 보호해 줍니다:

  • Timeouts — 우리는 절대 영원히 기다리지 않습니다.
  • Circuit breaker — 잠긴 문에 계속 들이받지 않습니다.
  • Bulkheads — “격실”을 구축해 배 전체가 침몰하지 않게 합니다.
  • 웹훅 스톰 방어 — 웹훅의 중복, 스파이크, 재시도가 현실임을 인정하고 대비합니다.

2. Timeouts: 우리는 영원히 기다리지 않는다

timeout이란 무엇이며, 왜 없으면 문제가 되는가

Timeout은 여러분의 코드가 의존 대상(데이터베이스, MCP 서버, 외부 HTTP API, 모델)으로부터 응답을 기다릴 최대 시간입니다. 지정한 시간을 넘겨도 응답이 오지 않으면 호출을 실패로 간주하고 자원을 해제하며 이해 가능한 오류나 fallback을 반환합니다.

타임아웃이 없으면 요청은 다음과 같은 문제가 생깁니다:

  • 무기한 대기,
  • 커넥션과 스레드 풀 점유,
  • 후속 요청 차단,
  • 연쇄 장애 유발.

원칙은 간단합니다: 35초 내 예측 가능한 실패가 5분의 설명 없는 침묵보다 낫습니다.

우리는 여러 수준의 타임아웃을 기억해야 합니다:

  • 프록시/로드밸런서 수준(Cloudflare, Nginx),
  • MCP Gateway 수준(마이크로서비스로의 HTTP 클라이언트),
  • 서비스 내부(데이터베이스, 외부 API, LLM 호출).

일반적으로 ChatGPT 맥락에서는 tool-call의 전체 시간을 일반 작업은 510초, 아주 무거운 작업도 최대 2030초 정도로 목표로 삼는 것이 합리적입니다. 그보다 길면 거의 확실히 나쁜 UX입니다.

간단한 fetchWithTimeout (TypeScript)

실전부터 시작해 봅시다. GiftGenius MCP Gateway에는 gift 추천기, commerce 서비스, 분석으로 호출하는 보조 HTTP 클라이언트가 있습니다. 표준 fetch를 타임아웃이 있는 함수로 감싸 보겠습니다:

// src/gateway/httpClient.ts
export async function fetchWithTimeout(
  url: string,
  opts: RequestInit & { timeoutMs?: number } = {}
) {
  const { timeoutMs = 5000, ...rest } = opts;
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    return await fetch(url, { ...rest, signal: controller.signal });
  } finally {
    clearTimeout(timeoutId);
  }
}

이제 Gateway 코드에서는 “맨몸” fetch를 쓰지 않고, 반드시 이 헬퍼를 통해 호출합니다:

// src/gateway/giftClient.ts
import { fetchWithTimeout } from "./httpClient";

export async function callGiftService(path: string) {
  const res = await fetchWithTimeout(
    process.env.GIFT_SERVICE_URL + path,
    { timeoutMs: 4000 }
  );

  if (!res.ok) {
    throw new Error(`gift_service_${res.status}`);
  }
  return res.json();
}

이 접근은 gift 서비스가 멈추더라도 4초 후 연결을 끊고 ChatGPT에 MCP 오류를 반환하게 해 줍니다. 연결을 끝까지 붙들고 있지 않습니다.

GiftGenius에서 타임아웃을 어디에 둘 것인가

우리 예시 GiftGenius에서:

  • Gateway 수준: Gift REST API, Commerce REST API, Analytics Service / REST API 호출에 타임아웃.
  • 각 서비스 내부: DB, ACP/결제, 외부 추천 API 호출에 타임아웃.
  • Gateway 진입부: ChatGPT에서 오는 요청에 대한 전체 타임아웃을 설정해 tool-call이 “영원한 스피너”가 되지 않게 합니다.

상위 수준의 대기 시간은 하위 수준보다 약간 더 길어야 합니다. 예를 들어 Gateway가 백엔드를 5초 기다리고, 백엔드가 DB를 3초 기다리면, 처리와 결과 직렬화에 대한 여유가 생깁니다.

타임아웃을 ChatGPT 모델에 어떻게 설명할 것인가

ChatGPT에는 의미 있는 오류를 반환하는 것이 중요합니다. 조용히 연결만 끊지 마세요. 추상적인 500 대신 모델이 사용자에게 전달할 수 있는 구조적 MCP 오류를 반환하는 편이 낫습니다: “선물 추천 서비스가 현재 과부하 상태입니다. 잠시 후 다시 시도해 주세요.” 등.

즉, Gateway에서 타임아웃이 나면 다음을 해야 합니다:

  1. AbortError 또는 우리 timeout_…을 포착합니다.
  2. 의미 있는 코드와 짧은 설명이 포함된 MCP 응답을 구성합니다.
  3. 모델이 이를 사람에게 어떻게 설명할지 선택할 수 있도록 합니다.

타임아웃은 멈춘 요청 문제를 해결하지만, 의존성이 대량으로 실패하기 시작하면 동일한 실패를 양산하는 폭주를 막지는 못합니다. 여기서는 다음 방어선인 circuit breaker가 필요합니다.

3. Circuit breaker: 죽어가는 서비스에 대한 “차단기”

직관: 왜 타임아웃만으로는 부족한가

우리는 이미 타임아웃으로 개별 호출의 대기 시간을 제한하는 법을 배웠습니다. 타임아웃은 한 건의 호출을 보호합니다. 하지만 의존성이 “완전히” 죽었다면(예: commerce 서비스가 매 요청마다 OOM(Out Of Memory)로 죽는 경우), 우리는 계속 그 서비스로 요청을 보낼 것이고, 매번 35초를 기다린 뒤 오류를 잡고, 네트워크와 CPU를 낭비하면서 또 기다리게 됩니다.

Circuit breaker(차단기)는 “기억”을 추가합니다. 오류와 타임아웃을 추적하다가 너무 많아지면 해당 서비스로 아예 요청을 보내지 않습니다. 대신 빠르게 실패를 반환하거나 fallback을 사용합니다. 일정 시간 뒤 half-open 모드로 조심스럽게 다시 시도합니다.

차단기의 고전적 상태:

  • Closed — 정상, 요청을 전송합니다.
  • Open — 서비스가 “죽었다”고 보고, 요청을 보내지 않고 즉시 오류를 반환합니다.
  • Half-open — 제한된 수의 요청으로 탐색합니다. 성공하면 closed로, 다시 실패하면 open으로 돌아갑니다.

간단한 circuit breaker 도식

간단한 다이어그램:

stateDiagram-v2
    [*] --> Closed
    Closed --> Open: 오류 과다
    Open --> HalfOpen: 쿨다운 만료
    HalfOpen --> Closed: 연속된 성공
    HalfOpen --> Open: 다시 오류
    Open --> Open: 빠른 거부

간단한 circuit breaker 구현 (TypeScript)

프로덕션에서는 보통 준비된 라이브러리를 사용합니다(Node.js에서는 예: opossum 또는 경량 자체 구현). 하지만 메커니즘을 이해하려면 작은 클래스로도 충분합니다.

commerce 모듈 호출 주위에 둔 매우 단순화된 breaker 예시:

// src/gateway/circuitBreaker.ts
type State = "closed" | "open" | "half-open";

export class CircuitBreaker {
    private state: State = "closed";
    private failureCount = 0;
    private nextAttemptAt = 0;

    constructor(
        private readonly failureThreshold = 5,
        private readonly cooldownMs = 30_000
    ) {}

    async call<T>(fn: () => Promise<T>): Promise<T> {
        const now = Date.now();

        if (this.state === "open") {
            if (now < this.nextAttemptAt) {
                throw new Error("circuit_open");
            }
            this.state = "half-open";
        }

        try {
            const result = await fn();
            this.onSuccess();
            return result;
        } catch (err) {
            this.onFailure();
            throw err;
        }
    }

    private onSuccess() {
        this.failureCount = 0;
        this.state = "closed";
    }

    private onFailure() {
        this.failureCount++;
        if (this.failureCount >= this.failureThreshold) {
            this.state = "open";
            this.nextAttemptAt = Date.now() + this.cooldownMs;
        }
    }
}

commerce 서비스 클라이언트에서의 사용 예:

// src/gateway/commerceClient.ts
const commerceBreaker = new CircuitBreaker(3, 20_000);

export async function callCommerce(path: string) {
    return commerceBreaker.call(async () => {
        const res = await fetchWithTimeout(
            process.env.COMMERCE_URL + path,
            { timeoutMs: 3000 }
        );
        if (!res.ok) throw new Error(`commerce_${res.status}`);
        return res.json();
    });
}

여기서 commerce가 대량 오류를 내거나 타임아웃에 자주 걸리면, 몇 번의 실패 후 breaker가 open으로 전환됩니다. 이 상태에서는 cooldownMs 동안 서비스로의 시도를 하지 않고 즉시 circuit_open 오류를 반환합니다.

breaker가 서비스를 “차단”했을 때 ChatGPT에서 무엇을 보여줄 것인가

ChatGPT 관점에서는 다음과 같으면 가장 좋습니다:

  • “commerce_unavailable” 또는 “gift_service_overloaded”와 같은 MCP 오류를 빠르게 반환합니다.
  • 이해 가능한 설명을 덧붙입니다: “결제 서비스가 일시적으로 사용 불가입니다. 잠시 후 다시 시도해 봅시다.”
  • 무한 재시도로 오류를 숨기지 않습니다.

이런 경우에는 “빠르고 솔직한 실패”가, 오래 버벅이다가 “문제가 발생했습니다”로 끝나는 것보다 훨씬 낫습니다. 특히 체크아웃에서 그렇습니다. 사용자는 40초 동안 스피너를 보는 것보다, 솔직한 메시지를 더 잘 받아들입니다.

타임아웃과 breaker는 “나쁜” 또는 죽어 있는 의존성으로부터 보호하지만, 한 종류의 부하가 모든 자원을 먹어 치워 시스템의 다른 부분을 질식시키는 문제는 해결하지 못합니다. 이를 위해 또 하나의 층—bulkheads—가 필요합니다.

4. Bulkheads: “격실”로 격리해 한 구역이 배 전체를 침몰시키지 않게

배의 비유

bulkhead 패턴은 배의 격벽에서 이름을 가져왔습니다. 한 격실에 구멍이 나도 물이 배 전체로 퍼지지 않습니다. 아키텍처에서는 이것이 자원을 분리한다는 뜻입니다. 특정 서비스가 과부하면 CPU, 커넥션, 풀을 모두 빨아들여 핵심 경로까지 죽이는 일을 막습니다.

마이크로서비스에서는 보통 다음을 분리합니다:

  • HTTP 커넥션 풀,
  • 스레드/워커 풀,
  • 큐/토픽,
  • 심지어 핵심 작업을 위한 별도 DB 클러스터.

아이디어는, 선물 추천 서비스가 느려지고 뻗더라도 그 서비스의 자원만 소진하고, 체크아웃과 인증은 망가지지 않게 하는 것입니다.

Node.js와 MCP Gateway에서의 Bulkheads

Node.js에는 전통적인 의미의 스레드는 없지만(event loop와 워커는 있습니다), 각 작업 축에 대해 동시 작업 수를 제한할 수 있습니다.

예: Gateway에는 세 가지 외부 의존성이 있습니다:

  • Gift 서비스(선물 추천, 무거운 LLM 호출).
  • Commerce 서비스(체크아웃, ACP).
  • Analytics 서비스(이벤트 로깅).

각각에 대해 동시 요청에 간단한 제한을 둘 수 있습니다.

예를 들어, 동시성을 제한하는 작은 “세마포어”:

// src/gateway/bulkhead.ts
export class Bulkhead {
    private active = 0;
    private queue: (() => void)[] = [];

    constructor(private readonly maxConcurrent: number) {}

    async run<T>(fn: () => Promise<T>): Promise<T> {
        if (this.active >= this.maxConcurrent) {
            await new Promise<void>((resolve) => this.queue.push(resolve));
        }
        this.active++;

        try {
            return await fn();
        } finally {
            this.active--;
            const next = this.queue.shift();
            if (next) next();
        }
    }
}

서비스에서의 사용:

// src/gateway/clients.ts
import { Bulkhead } from "./bulkhead";

const giftBulkhead = new Bulkhead(10);      // 최대 10개 동시 실행
const commerceBulkhead = new Bulkhead(3);   // 체크아웃은 강하게 제한
const analyticsBulkhead = new Bulkhead(50); // 넉넉히 가능

export async function callGiftWithBulkhead(fn: () => Promise<any>) {
    return giftBulkhead.run(fn);
}

export async function callCommerceWithBulkhead(fn: () => Promise<any>) {
    return commerceBulkhead.run(fn);
}

따라서 GPT가 “30개의 복잡한 선물 추천”을 대량으로 요청하더라도, 동시에 최대 10개만 실행됩니다. 체크아웃은 자체 제한으로 계속 동작할 수 있습니다.

GiftGenius: 어떤 격실이 필요할까

GiftGenius에서는 다음을 별도 격실로 두는 것이 합리적입니다:

  • 선물 추천(LLM이 무거움, 중요도 낮음, 느려져도 됨).
  • Checkout/ACP(초중요, 최대한 보호 필요).
  • 분석/로그(중요하지만 약간의 지연 허용).

더 발전된 아키텍처에서는 이들을 아예 별도 클러스터와 자원으로 배포합니다. 하지만 이 강의의 범위에서는 핵심 아이디어—부차 기능이 “산소를 다 빼앗지” 못하게—가 중요합니다.

이 세 가지 패턴(timeouts, circuit breaker, bulkheads)은 여러분이 외부 의존성으로 호출할 때 적용됩니다. 하지만 완벽히 설정된 아웃바운드 호출만으로는 막지 못하는, 유입 이벤트 스트림이라는 또 다른 위협이 있습니다. 가장 흔한 예가 웹훅 스톰입니다.

5. 웹훅 스톰: 세상이 여러분이 준비한 속도보다 더 자주 이벤트를 보낼 때

현실의 웹훅은 어떻게 동작하는가

탄력성의 네 번째 문제 원천은 유입 이벤트—ACP, Stripe 등에서 오는 웹훅—입니다. 타임아웃, circuit breaker, bulkhead를 이미 갖춰도, 이들이 진짜 “스톰”을 일으킬 수 있습니다.

웹훅은 “요청 시점”의 HTTP 요청이 아니라, 외부 시스템(Stripe, ACP, 외부 쇼핑몰 등)이 보내는 “push” 이벤트입니다. 불편한 속성이 몇 가지 있습니다:

  • 전달이 최소 한 번(at-least-once) 보장 — 즉, 중복은 피할 수 없습니다.
  • 전달 순서는 보장되지 않습니다.
  • 오류 시 재시도를 좋아합니다: 1초 뒤, 10초 뒤, 1분 뒤… 여러분이 2xx로 응답할 때까지.
  • 피크(예: 대규모 세일)에는 묶음으로 와서 “스톰”을 만듭니다.

핸들러가 멱등적이지 않고 처리 시간이 너무 길면 병목이 되어 전체 큐가 막히고, 재시도가 스톰을 더욱 키웁니다. 그 결과 DB, 큐, 워커 풀이 다운되고—연쇄적으로 나머지 시스템까지 무너질 수 있습니다.

스톰 방어의 기본 원칙

스톰에서 살아남을 확률을 크게 높여 주는 아이디어가 몇 가지 있습니다:

우선, queue-first, process-later. 유입 웹훅은 동기적으로 무거운 일을 수행해서는 안 됩니다. 가능한 한 빠르게 서명/포맷을 검증하고, 작업을 큐에 넣은 뒤 200 OK를 응답합니다. 처리는 워커가 비동기로 수행합니다. ChatGPT에 “빠른 확인”이 필요하면 별도 알림 경로를 둘 수 있습니다.

둘째, 핸들러의 멱등성. 같은 작업에 대한 반복 웹훅이 “주문을 다시 만들거나” “결제를 두 번 청구”해서는 안 됩니다. 보통 idempotency key 또는 eventId를 저장해 이미 처리했는지 확인합니다.

셋째, 수신 측 rate limiting과 circuit breaker. 발신자가 스톰을 일으키더라도, 여러분은 다음을 할 수 있습니다:

  • IP/구독/엔드포인트별 RPS 제한,
  • 429 또는 503을 일시적으로 반환해 재시도를 늦춤,
  • 깨진 다운스트림(예: 주문 DB)으로 흐름을 붓지 않도록 breaker 사용.

GiftGenius의 Next.js 웹훅 핸들러 예시

ACP/결제 시스템이 POST /api/commerce/webhook으로 주문 상태 웹훅을 보낸다고 가정합시다. 우리의 목표:

  • 이벤트를 빠르게 수신해 큐에 넣고,
  • 동기적으로 처리하지 않으며,
  • 중복에 무너지지 않는 것.

단순화된 예시(서명 검증과 실제 큐는 생략 — 이는 보안/큐 모듈에서 다룹니다):

// app/api/commerce/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";

// 여기는 Redis/큐가 올 자리이지만, 일단 배열로 흉내냅니다
const inMemoryQueue: any[] = [];
const processedEvents = new Set<string>(); // 멱등성 (데모용)

export async function POST(req: NextRequest) {
    const event = await req.json();

    const eventId = event.id as string;
    if (processedEvents.has(eventId)) {
        return NextResponse.json({ ok: true, duplicate: true });
    }

    // 실제로는 여기서 서명과 스키마를 검증합니다

    inMemoryQueue.push(event); // 백그라운드 처리를 위해 큐에 적재
    // 백그라운드 워커가 나중에 처리하고 ID를 처리 완료로 표시합니다
    return NextResponse.json({ ok: true });
}

이는 의사 구현이지만 두 가지가 중요합니다:

  1. 동기 부분은 최대한 가볍습니다.
  2. event.id를 둘러싼 멱등성을 설계합니다.

실전에서는 다음을 하게 됩니다:

  • 외부 큐(SQS, RabbitMQ, Kafka) 사용,
  • 처리한 이벤트를 DB에 저장,
  • 웹훅 시그니처와 페이로드 버전 검증,
  • 필요하다면 핸들러 주위에 별도의 Bulkhead/Breaker 적용.

GiftGenius 맥락에서는 어떻게 보일까

ACP/Stripe와 웹훅으로 통합된 GiftGenius에서는 성수기(연말, 블랙 프라이데이)에 스톰 방어가 특히 중요합니다. 그때는 이벤트가 많습니다:

  • 인텐트 생성,
  • 결제 승인,
  • 취소,
  • 환불.

핸들러가 길어지면(예: 외부 API 호출 때문에) 다음 위험이 있습니다:

  • ACP가 재시도를 시작하고,
  • 이벤트가 묶음으로 도착하며,
  • 주문 DB와 워커 풀이 포화됩니다.

“queue first” + 멱등성 + 입력 rate limiting은 바로 이런 시나리오에 대한 보험입니다.

6. 패턴들이 함께 작동하는 방식

이제 패턴들을 하나의 시나리오로 묶어 “선물 추천 후 바로 주문”이라는 실제 플로우에서 어떻게 동작하는지 보겠습니다.

“ChatGPT → Gateway → Gift Service → Commerce → 웹훅” 체인을 다음 시나리오로 생각해 봅시다:

사용자가 채팅에서 말합니다: “선물을 추천하고 바로 주문까지 진행해 줘.”

  1. 모델이 여러분의 tool suggest_and_checkout 호출을 결정합니다.
  2. Gateway는 fetchWithTimeout과 gift 서비스용 bulkhead를 통해 gift 서비스를 호출합니다.
  3. gift 서비스가 멈추면 타임아웃이 동작합니다. 그 주위의 breaker는 일정 횟수 오류 후 open으로 전환되고, 다음 요청은 “gift_service_unavailable” MCP 오류를 즉시 받습니다.
  4. gift 서비스가 응답하면, Gateway는 commerce 서비스를 호출합니다(다시 타임아웃과 별도 bulkhead 사용).
  5. commerce 문제는 별도의 circuit breaker가 처리합니다. checkout이 중요하므로 gift보다 더 엄격하게 설정합니다.
  6. 성공한 주문은 ACP에서 여러분의 /api/commerce/webhook으로 웹훅을 보냅니다. 핸들러는 이벤트를 큐에 넣고 빠르게 응답하며, 백그라운드 워커가 처리합니다. 동일 eventId의 반복 웹훅은 중복으로 무시됩니다.

결과적으로:

  • 멈춘 추천 서비스가 체크아웃을 죽이지 않습니다.
  • 멈춘 commerce 때문에 모든 tool-calls가 1분짜리 스피너로 변하지 않습니다 — ChatGPT는 의미 있는 오류를 빠르게 받습니다.
  • 웹훅 스톰이 기본 HTTP 경로를 망가뜨리지 않습니다.
  • 어디서 성능을 낮출지 통제할 수 있습니다: 결제를 죽이는 대신, 개인화 추천을 잠시 끄는 편이 낫습니다.

7. 여러분의 App을 위한 작은 실전 체크리스트(서술형)

요약하면, MCP/Gateway가 있는 전형적인 ChatGPT App에서는 다음 질문들을 순서대로 점검하는 것이 좋습니다.

먼저, 모든 외부 호출에 타임아웃이 있는지 확인합니다. 모든 fetch 코드, DB 및 LLM 요청은 fetchWithTimeout 같은 래퍼를 통해 적절한 값으로 호출되어야 합니다. 어디에도 “무한 대기” 지점이 없어야 합니다.

그다음, 가장 취약한 의존성을 식별합니다. 보통 결제, ACP, 대형 외부 API, 때로는 주문 DB가 해당됩니다. 이들 주위에 circuit breaker를 두어, 이미 죽은 서비스로의 반복 호출 폭주를 막습니다. 동시에 breaker가 open일 때 ChatGPT가 어떻게 행동할지 미리 정의합니다.

이후, 자원을 “격실” 관점에서 봅니다. 모든 것이 하나의 커넥션 풀과 워커 풀을 공유하는지, 아니면 핵심 작업(로그인, 체크아웃)이 추천/분석 서비스와 독립적인 동시성 제한을 갖는지 확인합니다. 아니라면, 최소한 동시 작업 수 제한 같은 가장 단순한 bulkhead 구현부터 추가합니다.

마지막으로, 모든 수신 웹훅을 감사합니다. idempotency key 또는 eventId가 있는지, HTTP 핸들러에서 무거운 작업을 동기적으로 실행하지는 않는지, 다운스트림이 일시적으로 죽었을 때 재시도 파도를 견딜 수 있는지 점검합니다. 아니라면, 로직을 큐와 백그라운드 워커로 옮깁니다.

이런 단계만으로도 매우 복잡한 인프라 없이 탄력성을 크게 끌어올릴 수 있습니다.

8. timeouts, circuit breakers, bulkheads, 웹훅 스톰과 관련한 흔한 실수

실수 №1: “아래쪽 어딘가”에 타임아웃이 없음.
개발자는 종종 Gateway나 프런트엔드에만 타임아웃을 두고, 백엔드 내부의 DB, 외부 API, LLM을 잊습니다. 결과적으로 외부 요청은 5초 타임아웃이 있는 것처럼 보여도, 내부의 한 DB/결제 호출이 수분간 대기하며 커넥션 풀을 막고 연쇄 장애를 유발합니다.

실수 №2: “혹시 몰라서”의 거대한 타임아웃.
가끔 60120초 같은 큰 타임아웃을 둡니다. ChatGPT 맥락에서는 거의 항상 나쁩니다. 사용자는 떠나고, 모델은 환각을 시작하며, 여러분의 자원은 그동안 묶여 있습니다. 510초 내의 솔직한 실패와 이해 가능한 설명이 훨씬 낫습니다.

실수 №3: UX를 고민하지 않은 circuit breaker.
가끔 체크리스트용으로 breaker만 추가하고, 동작 시 사용자나 모델에게 이해 불가능한 500, “ECONNREFUSED”, “axios error”가 떨어집니다. GPT는 상황을 제대로 설명하지 못하고 지어내기 시작합니다. 사람과 모델 모두에게 이해 가능한 오류 문구를 미리 고민해 두어야 합니다.

실수 №4: bulkhead 없이 자원을 뒤섞음.
전형적 시나리오: 추천(또는 분석) 서비스가 느려져 DB 커넥션 풀이나 스레드 풀을 모두 먹고, 그 뒤로 체크아웃과 로그인까지 죽습니다. 자원이 분리되지 않았기 때문입니다. bulkhead 접근이 없으면 부차 기능이 전체 프로덕션을 죽일 수 있습니다.

실수 №5: 웹훅을 일반 요청처럼 처리.
초보자는 종종 웹훅 핸들러를 일반 컨트롤러처럼 작성합니다: 긴 비즈니스 로직, 외부 API 호출, 멱등성 부재. 재시도와 중복이 있는 상황에서는 이벤트 이중 처리, 이상한 주문 상태, 스톰 시 부하로 다운되는 문제가 생깁니다.

실수 №6: 커머스 시나리오에서 멱등성을 무시.
특히 위험한 것은 결제 웹훅이 주문을 다시 만들거나 상태를 다시 변경하는 경우입니다. idempotency key 검증과 이벤트 처리 상태 저장이 없으면, 언젠가 이중 청구나 중복 주문을 맞이하게 됩니다.

실수 №7: setTimeout과 “마법의 지연”으로 모든 걸 해결하려 함.
가끔 레이스 컨디션과 스톰 문제를 “100ms만 기다리면 괜찮아진다”로 우회하려 합니다. 실제로는 동작을 더 불안정하게 만들 뿐이며, 진짜 장애로부터 보호하지 못합니다. 정석은 명시적 타임아웃, circuit breaker, 큐, 멱등성이지, 지연에 의존한 요행이 아닙니다.

실수 №8: 핵심 경로에 대한 우선순위 부재.
체크아웃과 로그인이 분석이나 추천 로직과 같은 제한을 공유하면, 어떤 과부하도 핵심과 비핵심을 똑같이 쓰러뜨립니다. 탄력적인 설계에서 checkout과 auth는 “절대 사수” 대상입니다: 별도 자원, 별도 제한, 별도 알림과 SLO.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION