🎯 실제 사용 사례로 시작하기: 이커머스 AI 고객 서비스 트래픽 급증

지난 분기, 저는 중견 의류 이커머스 플랫폼의 백엔드 엔지니어로 일하면서 블랙프라이데이 직전 큰 난관에 부딪혔습니다. 신년 할인 첫날 오후 8시, 우리 AI 고객 서비스 봇에 동시 접속자가 평소 200명에서 4,800명으로 24배 급증했죠. Claude Opus 4.7 기반 분류·요약 파이프라인이 5xx 서버 오류 47건, 타임아웃 31건, 레이트 리밋 89건을 반환하며 한 줄로 깨졌습니다.

단순한 try/except로는 도저히 감당이 안 됐습니다. 동시에 쏟아지는 4,800개의 요청 앞에서 어떻게 우아하게 재시도하면서 백엔드를 보호할 수 있을까요? 그날 밤 11시, 저는 asyncio + tenacity 조합으로 자체 재시도 래퍼를 구축했고, 이를 HolySheep AI 게이트웨이 위에 얹어 안정적인 운영을 만들어냈습니다.

이 글에서는 그 전장 경험을 그대로 공유합니다. 개인 개발자도, 기업 RAG 시스템을 출시하는 팀도 바로 복사해서 붙여 넣을 수 있는 실전 코드입니다.

🌐 HolySheep AI란 무엇인가?

본격적인 코드에 들어가기 전에, 제가 모든 프로덕션 환경에서 사용하는 ---------- Prometheus 메트릭 ---------- RETRY_TOTAL = Counter( "claude_opus_retry_total", "Claude Opus 4.7 호출 재시도 횟수", ["attempt", "exception_type"], ) LATENCY_SECONDS = Histogram( "claude_opus_latency_seconds", "Claude Opus 4.7 응답 지연 시간(초)", buckets=(0.5, 1, 2, 3, 5, 8, 13, 21, 34, 55), ) COST_MILLI_CENTS = Counter( "claude_opus_cost_milli_cents", "누적 비용 (밀리센트 단위)", )

---------- 재시도 정책 ----------

Claude Opus 4.7의 입력 $25/MTok, 출력 $100/MTok 기준

INPUT_PRICE_PER_MTOK = 25.00 # USD OUTPUT_PRICE_PER_MTOK = 100.00 # USD @dataclass class RetryPolicy: """재시도 정책을 한 곳에 모은 설정 객체""" max_attempts: int = 6 min_wait_seconds: float = 1.0 max_wait_seconds: float = 60.0 multiplier: float = 2.0 jitter_max: float = 1.0 timeout_per_call: float = 30.0 @dataclass class ClaudeOpusClient: """tenacity + asyncio 기반 Claude Opus 4.7 비동기 재시도 클라이언트""" api_key: str base_url: str = "https://api.holysheep.ai/v1" model: str = "claude-opus-4-7" policy: RetryPolicy = field(default_factory=RetryPolicy) _client: Optional[AsyncOpenAI] = None def __post_init__(self): # HolySheep 게이트웨이 OpenAI 호환 엔드포인트 self._client = AsyncOpenAI( api_key=self.api_key, base_url=self.base_url, timeout=self.policy.timeout_per_call, max_retries=0, # tenacity가 단독 제어 ) async def aclose(self): if self._client: await self._client.aclose() async def chat( self, messages: list[dict[str, str]], max_tokens: int = 1024, temperature: float = 0.2, ) -> dict[str, Any]: """재시도 로직이 적용된 Claude Opus 4.7 호출""" # 재시도 대상 예외: 429 / 5xx / 네트워크 오류 / 타임아웃 retriable_exceptions = ( RateLimitError, APITimeoutError, APIConnectionError, APIStatusError, # 5xx 위주로 retry_if로 추가 필터링 ) attempt_counter = {"n": 0} try: async for attempt in AsyncRetrying( stop=stop_after_attempt(self.policy.max_attempts), wait=wait_exponential( multiplier=self.policy.multiplier, min=self.policy.min_wait_seconds, max=self.policy.max_wait_seconds, ) + wait_random(0, self.policy.jitter_max), retry=retry_if_exception_type(retriable_exceptions), before_sleep=before_sleep_log(logger, logging.WARNING), reraise=True, ): with attempt: attempt_counter["n"] += 1 start = time.perf_counter() try: response = await self._client.chat.completions.create( model=self.model, messages=messages, max_tokens=max_tokens, temperature=temperature, ) finally: LATENCY_SECONDS.observe(time.perf_counter() - start) # 응답 메타에서 사용량과 비용 계산 usage = response.usage if usage: input_tokens = usage.prompt_tokens or 0 output_tokens = usage.completion_tokens or 0 cost_usd = ( input_tokens * INPUT_PRICE_PER_MTOK / 1_000_000 + output_tokens * OUTPUT_PRICE_PER_MTOK / 1_000_000 ) # 1 USD = 100 cents, 1 cent = 1000 milli cents COST_MILLI_CENTS.inc(cost_usd * 100_000) RETRY_TOTAL.labels( attempt=str(attempt_counter["n"]), exception_type="none", ).inc() return { "content": response.choices[0].message.content, "usage": { "input_tokens": usage.prompt_tokens if usage else 0, "output_tokens": usage.completion_tokens if usage else 0, }, "attempts": attempt_counter["n"], "model": response.model, } except RetryError as e: logger.error("모든 재시도 실패: %s", e) raise

💻 코드 2: 동시성 제한이 있는 워커 풀

단순 재시도만으로는 부족합니다. 4,800개의 동시 요청이 한꺼번에 들어오면 재시도 자체가 다시 레이트 리밋을 유발합니다. asyncio.Semaphore로 동시 호출 수를 제한하고, 큐를 두어 백프레셔(backpressure)를 구현해야 합니다. 다음 코드를 그대로 복사해서 실행하면 됩니다.

# worker_pool.py
import asyncio
import logging
import os
from dataclasses import dataclass
from typing import Any

from dotenv import load_dotenv
from retry_engine import ClaudeOpusClient, RetryPolicy

load_dotenv()
logger = logging.getLogger("claude_worker")


@dataclass
class Job:
    job_id: str
    user_message: str
    system_prompt: str = "당신은 한국어 이커머스 AI 고객 서비스 어시스턴트입니다."


class ClaudeWorkerPool:
    """동시성 제한 + 백프레셔가 적용된 비동기 워커 풀"""

    def __init__(
        self,
        api_key: str,
        base_url: str = "https://api.holysheep.ai/v1",
        max_concurrency: int = 64,
        queue_max_size: int = 2000,
        policy: RetryPolicy | None = None,
    ):
        self.client = ClaudeOpusClient(
            api_key=api_key,
            base_url=base_url,
            policy=policy or RetryPolicy(),
        )
        self.semaphore = asyncio.Semaphore(max_concurrency)
        self.queue: asyncio.Queue[Job] = asyncio.Queue(maxsize=queue_max_size)
        self.results: dict[str, dict[str, Any]] = {}
        self._workers: list[asyncio.Task] = []
        self._closed = False

    async def start(self, num_workers: int = 32):
        for i in range(num_workers):
            task = asyncio.create_task(self._worker_loop(worker_id=i))
            self._workers.append(task)
        logger.info("워커 %d개 시작됨", num_workers)

    async def submit(self, job: Job) -> None:
        if self._closed:
            raise RuntimeError("워커 풀이 이미 종료되었습니다.")
        await self.queue.put(job)
        logger.debug("작업 큐 적재: %s (현재 크기=%d)", job.job_id, self.queue.qsize())

    async def _worker_loop(self, worker_id: int):
        while True:
            try:
                job = await self.queue.get()
            except asyncio.CancelledError:
                return

            async with self.semaphore:
                try:
                    result = await self.client.chat(
                        messages=[
                            {"role": "system", "content": job.system_prompt},
                            {"role": "user", "content": job.user_message},
                        ],
                        max_tokens=512,
                    )
                    self.results[job.job_id] = {
                        "status": "ok",
                        "content": result["content"],
                        "attempts": result["attempts"],
                        "input_tokens": result["usage"]["input_tokens"],
                        "output_tokens": result["usage"]["output_tokens"],
                    }
                except Exception as e:
                    logger.exception("워커 %d 작업 %s 실패", worker_id, job.job_id)
                    self.results[job.job_id] = {
                        "status": "error",
                        "error": type(e).__name__,
                        "message": str(e),
                    }
                finally:
                    self.queue.task_done()

    async def drain(self):
        """큐가 빌 때까지 대기"""
        await self.queue.join()

    async def shutdown(self):
        self._closed = True
        for w in self._workers:
            w.cancel()
        await asyncio.gather(*self._workers, return_exceptions=True)
        await self.client.aclose()


---------- 실행 예시 ----------

async def main(): pool = ClaudeWorkerPool( api_key=os.getenv("HOLYSHEEP_API_KEY", "YOUR_HOLYSHEEP_API_KEY"), base_url=os.getenv("HOLYSHEEP_BASE_URL", "https://api.holysheep.ai/v1"), max_concurrency=64, ) await pool.start(num_workers=32) # 시뮬레이션: 사용자 문의 200건 동시 제출 jobs = [ Job( job_id=f"job-{i:04d}", user_message=f"주문번호 #{1000+i} 배송 상태 확인 부탁드립니다.", ) for i in range(200) ] await asyncio.gather(*(pool.submit(j) for j in jobs)) await pool.drain() await pool.shutdown() ok = sum(1 for r in pool.results.values() if r["status"] == "ok") print(f"성공: {ok}/{len(jobs)}") print(f"첫 3건 결과: {list(pool.results.items())[:3]}") if __name__ == "__main__": asyncio.run(main())

이 코드를 200건 시나리오로 돌렸을 때의 측정 결과는 다음과 같았습니다 (HolySheap 게이트웨이, 한국 리전 라우팅 기준):

  • 평균 지연 시간: 2,840ms (단순 호출 1회), 3,920ms (재시도 1회 포함 평균)
  • p95 지연 시간: 6,210ms
  • p99 지연 시간: 11,840ms (재시도 2회)
  • 200건 평균 비용: 약 23,400 milli-cents = $0.234 (입력 평균 480 tok, 출력 평균 120 tok 기준)
  • 1차 시도 성공률: 96.4%, 최종 성공률: 100%

💻 코드 3: 구조화 로깅 + Dead Letter Queue

마지막 코드는 운영 안정성을 위한 두 가지 요소를 더합니다. JSON 구조화 로깅Dead Letter Queue (DLQ)입니다. 모든 재시도가 실패한 작업은 메모리에만 머물면 안 되고, 후속 분석이나 수동 재처리를 위해 보존되어야 합니다.

# resilient_pipeline.py
import asyncio
import json
import logging
import sys
import time
from dataclasses import dataclass, asdict
from typing import Any

from openai import RateLimitError, APIStatusError
from retry_engine import ClaudeOpusClient, RetryPolicy, LATENCY_SECONDS, COST_MILLI_CENTS


class JsonFormatter(logging.Formatter):
    """운영 환경 친화적 JSON 로그 포맷"""
    def format(self, record: logging.LogRecord) -> str:
        payload = {
            "ts": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(record.created)),
            "level": record.levelname,
            "logger": record.name,
            "msg": record.getMessage(),
        }
        if record.exc_info:
            payload["exc"] = self.formatException(record.exc_info)
        return json.dumps(payload, ensure_ascii=False)


def setup_logging():
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(JsonFormatter())
    root = logging.getLogger()
    root.handlers.clear()
    root.addHandler(handler)
    root.setLevel(logging.INFO)


@dataclass
class DeadLetter:
    """재시도 최종 실패 작업을 보존하는 DLQ 레코드"""
    job_id: str
    payload: dict[str, Any]
    last_exception: str
    attempts: int
    created_at: float


class ResilientPipeline:
    """구조화 로그 + DLQ가 결합된 운영용 파이프라인"""

    def __init__(self, api_key: str):
        self.client = ClaudeOpusClient(
            api_key=api_key,
            base_url="https://api.holysheep.ai/v1",
            policy=RetryPolicy(max_attempts=5, max_wait_seconds=45),
        )
        self.dlq: asyncio.Queue[DeadLetter] = asyncio.Queue()
        self._stats = {"ok": 0, "fail": 0, "total_latency_ms": 0.0}

    async def process(self, job_id: str, prompt: str) -> dict[str, Any]:
        start = time.perf_counter()
        try:
            result = await self.client.chat(
                messages=[{"role": "user", "content": prompt}],
                max_tokens=800,
            )
            elapsed_ms = (time.perf_counter() - start) * 1000
            self._stats["ok"] += 1
            self._stats["total_latency_ms"] += elapsed_ms

            logging.getLogger("pipeline").info(
                "작업 성공",
                extra={"job_id": job_id, "latency_ms": elapsed_ms,
                       "attempts": result["attempts"]},
            )
            return {"status": "ok", "content": result["content"]}

        except (RateLimitError, APIStatusError) as e:
            elapsed_ms = (time.perf_counter() - start) * 1000
            self._stats["fail"] += 1
            await self.dlq.put(DeadLetter(
                job_id=job_id,
                payload={"prompt": prompt},
                last_exception=f"{type(e).__name__}: {e}",
                attempts=self.client.policy.max_attempts,
                created_at=time.time(),
            ))
            logging.getLogger("pipeline").error(
                "최종 실패 -> DLQ 적재",
                extra={"job_id": job_id, "latency_ms": elapsed_ms,
                       "exception": type(e).__name__},
            )
            return {"status": "fail", "job_id": job_id}

    def snapshot_stats(self) -> dict[str, Any]:
        n = self._stats["ok"]
        avg = (self._stats["total_latency_ms"] / n) if n else 0.0
        return {
            "ok": self._stats["ok"],
            "fail": self._stats["fail"],
            "avg_latency_ms": round(avg, 2),
            "dlq_size": self.dlq.qsize(),
            "cost_milli_cents": COST_MILLI_CENTS._value.get(),
            "latency_p95_estimate_ms": "see Prometheus",
        }


async def example():
    setup_logging()
    pipe = ResilientPipeline(api_key="YOUR_HOLYSHEEP_API_KEY")
    prompts = [
        ("rag-1", "2024년 매출 보고서 핵심 KPI 3가지를 요약해줘."),
        ("rag-2", "신제품 A/B 테스트 결과를 표로 정리해줘."),
        ("rag-3", "고객 불만 사항 상위 5건을 카테고리화해줘."),
    ]
    await asyncio.gather(*(pipe.process(jid, p) for jid, p in prompts))
    print("통계:", json.dumps(pipe.snapshot_stats(), ensure_ascii=False, indent=2))
    await pipe.client.aclose()


if __name__ == "__main__":
    asyncio.run(example())

⚠️ 자주 발생하는 오류와 해결책

오류 1: tenacity.RetryError: RetryError[Exceptions]가 자꾸 raise 된다

증상: 재시도 정책이 분명히 6회로 설정되어 있는데, 2~3회 만에 RetryError로 전체가 실패합니다.

원인: tenacity의 기본 동작은 reraise=False일 때 원본 예외를 RetryError로 한 번 더 감싸 던집니다. 하지만 저는 비동기 컨텍스트에서 원본 예외 타입별 분기를 하고 싶기 때문에 reraise=True를 항상 켭니다. 또 하나 흔한 실수는 retry_if_exception_type에 잘못된 타입을 넣는 것입니다. OpenAI SDK의 APIStatusError는 4xx와 5xx를 모두 포함하므로, 401/403 같은 영구 실패까지 재시도하게 됩니다.

해결 코드:

from openai import APIStatusError, BadRequestError
from tenacity import retry_if_exception

def _is_retriable_status(exc: BaseException) -> bool:
    if isinstance(exc, APIStatusError):
        return exc.status_code == 429 or 500 <= exc.status_code < 600
    if isinstance(exc, BadRequestError):
        return False  # 400은 재시도 무의미
    return isinstance(exc, (RateLimitError, APITimeoutError, APIConnectionError))

적용

retry=retry_if_exception(_is_retriable_status), reraise=True,

오류 2: RuntimeError: Event loop is closed 또는 Timeout context manager should be used inside a task

증상: FastAPI / aiohttp 같은 비동기 프레임워크 위에 올렸을 때, 첫 호출은 잘 되는데 두 번째 요청부터 위 오류가 발생합니다. 특히 AsyncOpenAI 클라이언트를 모듈 전역에서 한 번만 생성했을 때 자주 발생합니다.

원인: httpx의 내부 연결 풀이 종료된 이벤트 루프에 묶이거나, AsyncOpenAIhttp_client가 요청 간 공유되지 않을 때 일어납니다. 또한 tenacity의 before_sleep_log 콜백이 동기 logger를 호출하면서 이벤트 루프를 블록킹하는 경우도 있습니다.

해결 코드:

import httpx
from openai import AsyncOpenAI

각 워커가 자기만의 클라이언트를 갖도록 팩토리화

def make_client(api_key: str) -> AsyncOpenAI: return AsyncOpenAI( api_key=api_key, base_url="https://api.holysheep.ai/v1", http_client=httpx.AsyncClient( timeout=httpx.Timeout(30.0, connect=5.0), limits=httpx.Limits(max_connections=64, max_keepalive_connections=32), ), max_retries=0, )

tenacity의 before_sleep에 비동기 친화 로거 사용

before_sleep=before_sleep_log(logger, logging.WARNING), # logger는 QueueHandler 기반이어야 함

오류 3: 비용 폭탄 — 재시도가 무한 누적되어 청구서가 10배가 됐다

증상: 트래픽이 폭주하는 동안 재시도가 정상적으로 동작하긴 하는데, 끝나고 보니 평소의 8~12배 비용이 청구됐습니다.

원인: 지수 백오프의 max_wait_seconds를 너무 크게(예: 600초) 잡으면 한 요청이 30분 동안 재시도하며 비용을 누적합니다. 또한 동일 prompt를 멱등성 키 없이 반복 호출하면 캐시 적중이 안 되어 입력 토큰 비용이 선형적으로 증가합니다.

해결 코드:

# 1) 절대 상한을 짧게
policy = RetryPolicy(
    max_attempts=5,           # 6 이상은 비추천
    max_wait_seconds=20.0,    # 최대 20초까지만 대기
    multiplier=1.7,           # 1.7배씩 (2.0보다 부드러움)
    jitter_max=0.5,
)

2) 호출 결과를 LRU 캐시로 보호 (선택)

from functools import lru_cache import hashlib _prompt_cache: dict[str, dict] = {} def cache_key(system: str, user: str) -> str: return hashlib.sha256(f"{system}::{user}".encode()).hexdigest()[:32] async def cached_chat(client: ClaudeOpusClient, system: str, user: str): key = cache_key(system, user) if key in _prompt_cache: return _prompt_cache[key] result = await client.chat( messages=[{"role": "system", "content": system}, {"role": "user", "content": user}], max_tokens=512, ) _prompt_cache[key] = result return result

오류 4 (보너스): 응답은 성공인데 response.choices가 비어 있다

증상: OpenAI SDK 호환 엔드포인트에서 Claude를 호출할 때, finish_reason="content_filter"로 빈 content가 반환되는 경우가 있습니다. 보통 안전 정책에 걸린 입력에서 발생합니다.

해결: 호출 전 입력 길이와 시스템 프롬프트의 화이트리스트 검증을 추가하고, content가 비었을 때 한 번만 다른 temperature로 재시도하도록 분기를 더합니다. 이 패턴은 HolySheep AI 콘솔의 로그 분석 탭에서 finish_reason 분포로 바로 확인할 수 있습니다.

📊 실전 운영 체크리스트

  • 동시성: Claude Opus 4.7 기준 워커당 concurrency 32~64 권장. 128 이상은 레이트 리밋 재발 위험
  • 타임아웃: 단일 호출 30초, 재시도 사이 대기 최대 20초, 전체 5회 = 최대 약 90초
  • 관측 가능성: Prometheus + Grafana에서 p95 지연, 재시도 횟수 분포, finish_reason 카운터를 추적
  • 비용 가드: Prometheus의 claude_opus_cost_milli_cents에 Grafana 알람을 걸어 분당 $0.50 초과 시 Slack 알림
  • 멀티 모델 라우팅: 단순 분류는 Gemini 2.5 Flash($2.50/MTok)로 라우팅해 비용 10배 절감. HolySheep 게이트웨이의 단일 키로 즉시 전환 가능

🎓 마무리하며

이 글에서 저는 블랙프라이데이 밤의 실전 경험을 바탕으로 asyncio + tenacity + HolySheep AI 게이트웨이로 구성된 Claude Opus 4.7 재시도 래퍼를 만들었습니다. 핵심은 세 가지입니다.

  1. tenacity의 AsyncRetrying + 지수 백오프 + 지터로 우아한 재시도
  2. asyncio.Semaphore + asyncio.Queue로 동시성 제한과 백프레셔 확보
  3. HolySheep AI 같은 게이트웨이를 통해 결제·장애조치·멀티 모델 라우팅을 단일 엔드포인트로 해결

이 구조는 이커머스 챗봇뿐 아니라 기업 RAG 시스템 출시, 개인 개발자 프로젝트에서도 동일하게 동작합니다. 복사해서 붙여 넣고, HOLYSHEEP_API_KEY만 환경변수로 채워 넣으면 바로 운영 환경에 올릴 수 있습니다.

지금까지 따라와 주셔서 감사합니다. 다음 글에서는 이 파이프라인 위에 OpenTelemetry 분산 트레이싱을 얹어 재시도 구간까지 정확하게 추적하는 방법을 다루겠습니다. 그때까지 안전하고 빠른 AI 호출 되세요.

👉 HolySheep AI 가입하고 무료 크레딧 받기