나는 광대가 된 기분이었다, 5개의 라이브러리를 연결해 탄력적인 API 클라이언트를 만들기 위해
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
문서:
저장소: