나는 광대가 된 기분이었다, 5개의 라이브러리를 연결해 탄력적인 API 클라이언트를 만들기 위해

발행: (2026년 2월 5일 오후 10:11 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

그래서 모든 것을 통합하는 클라이언트를 만들었습니다.

저는 단순한 API 클라이언트만 원했습니다.

import httpx

async def fetch_user(user_id: str):
    async with httpx.AsyncClient() as client:
        r = await client.get(f"https://api.example.com/users/{user_id}")
        return r.json()

그것은 약 5분 정도 지속됐습니다.

실제 API는:

  • 요청 제한(rate limit)을 걸어옵니다
  • 시간 초과
  • 503 오류를 반환합니다
  • 가끔 완전히 죽어버립니다
  • 그리고 재시도는 자체 서비스에 DDoS를 일으킬 수 있습니다

그래서 모든 파이썬 개발자가 하는 대로, 라이브러리를 계속 쌓기 시작했습니다.

데코레이터 파멸의 탑

First: rate limiting.
Then: retry.
Then: circuit breaker.

@breaker
@retry(...)
@sleep_and_retry
@limits(...)
async def fetch_user(...):
    ...

And I hated it.

Not because it didn’t work — but because it didn’t scale.

Problems

  • 3+ libraries
  • fragile decorator ordering
  • conflicting abstractions
  • async quirks
  • painful testing
  • scattered observability
  • dependency sprawl

And this was for one function. Imagine 10 APIs, per‑user limits, background jobs, webhooks. You’re no longer writing business logic; you’re babysitting resilience glue code.

아이디어: 파이프라인으로서의 복원력

복원력이 데코레이터의 혼합이 아니라면 어떨까요? 모든 호출이 단일 오케스트레이터를 통해 흐른다면 어떨까요?

from limitpal import (
    AsyncResilientExecutor,
    AsyncTokenBucket,
    RetryPolicy,
    CircuitBreaker,
)

executor = AsyncResilientExecutor(
    limiter=AsyncTokenBucket(capacity=10, refill_rate=100 / 60),
    retry_policy=RetryPolicy(max_attempts=3),
    circuit_breaker=CircuitBreaker(failure_threshold=5),
)

result = await executor.run("user:123", api_call)

데코레이터가 없습니다. 라이브러리를 겹쳐 쓰지 않습니다. 깨지기 쉬운 접착제도 없습니다. 하나의 실행 파이프라인. 바로 LimitPal이 바로 그것입니다.

LimitPal이 실제로 제공하는 것

LimitPal은 탄력적인 Python 클라이언트와 서비스를 구축하기 위한 툴킷입니다. 다음을 결합합니다:

  • 레이트 리밋(토큰 / 리키 버킷)
  • 지수 백오프 + 지터를 이용한 재시도
  • 서킷 브레이커
  • 조합 가능한 제한기
  • 모든 것을 조정하는 레질리언스 실행기

또한 다음을 제공합니다:

  • 완전한 async + sync 지원
  • 의존성 제로
  • 기본적으로 스레드‑안전
  • 테스트를 위한 결정론적 시간 제어
  • 키‑기반 격리(사용자별 / IP별 / 테넌트별)

목표는 더 많은 기능이 아니라 구성 요소를 최소화하는 것입니다.

회복성 파이프라인 (핵심 아이디어)

Every call goes through:

Circuit breaker check
→ Rate limiter
→ Execute + retry loop
→ Record result

The ordering matters. You’re not just “adding retry”; you’re designing failure behavior as a system:

  • breaker stops cascading failures
  • limiter protects infrastructure
  • retry handles temporary issues
  • executor keeps it coherent

One mental model instead of five.

아무도 말하지 않는 테스트 문제

시간 기반 로직은 테스트하기가 매우 힘듭니다.

전통적인 접근 방식:

time.sleep(1)

느림. 불안정함. 비결정적.

LimitPal은 플러그인 가능한 시계를 사용하므로 테스트는 다음과 같이 됩니다:

clock.advance(1.0)

즉시. 결정적. 밀리초 단위로 수분에 달하는 재시도를 시뮬레이션할 수 있습니다. 신뢰성을 중시하는 팀에게는 이것이 게임 체인저입니다.

실제 예시

A resilient HTTP client in ~10 lines:

executor = AsyncResilientExecutor(
    limiter=AsyncTokenBucket(capacity=10, refill_rate=100 / 60),
    retry_policy=RetryPolicy(max_attempts=3, base_delay=0.5),
    circuit_breaker=CircuitBreaker(failure_threshold=5),
)

async def fetch():
    return await httpx.get("https://api.example.com")

result = await executor.run("api", fetch)

You automatically get:

  • 버스트 제어
  • 지수 백오프 재시도
  • 연쇄 실패 방지
  • 깔끔한 비동기 의미론

데코레이터 탑이 없습니다.

언제 사용해야 할까요?

LimitPal을 사용하세요:

  • API 클라이언트를 구축할 때
  • 불안정한 서드파티 서비스를 호출할 때
  • 백그라운드 작업을 실행할 때
  • 사용자별 제한이 필요할 때
  • 결정론적 테스트를 중요시할 때
  • 깔끔한 async 지원이 필요할 때

재시도만 필요하다면, 더 작은 라이브러리로 충분합니다. 구성이 필요하다면, 그것이 바로 이 라이브러리의 특화된 영역입니다.

Part 2: 내부

이 게시물은 아이디어에 관한 것입니다. Part 2에서는 다음을 깊이 파고들겠습니다:

  • 실행기 파이프라인이 작동하는 방식
  • 서킷 브레이커 상태 머신
  • 시계 추상화 설계
  • 복합 제한기 아키텍처
  • 실패 모델링

왜냐하면 탄력성은 마법이 아니기 때문입니다. 그것은 설계입니다.

시도해 보기

pip install limitpal

문서:
저장소:

Back to Blog

관련 글

더 보기 »

파이썬 Day 2 (연산자와 조건문)

Operators 연산자는 값과 변수에 대해 연산을 수행하는 데 사용됩니다. 연산자 유형 !Operators overview https://media2.dev.to/dynamic/image/width=800...