Python으로 레질리언스 엔진 구축: LimitPal 내부 구조 (파트 2)

발행: (2026년 2월 6일 오후 08:26 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

개요

Executor 파이프라인, 시계 추상화, 그리고 circuit‑breaker 아키텍처는 LimitPal의 핵심입니다. 설계는 모든 호출이 동일한 단계들을 고정된 순서대로 통과하는 단일 실행 파이프라인을 따릅니다:

  1. Circuit breaker
  2. Rate limiter
  3. Retry loop
  4. Result recording

이 순서는 의도된 것으로, 빠르게 실패하도록 하여 상위 서비스가 다운되었을 때 즉시 오류를 반환하고, breaker 실패가 할당량을 소모하는 것을 방지하며, 재시도가 레이트 제한을 준수하도록 보장하고, 버스트 동작을 예측 가능하게 유지합니다.

실행 파이프라인

  • 빠른 실패: 회로 차단기가 먼저 실행됩니다. 업스트림 서비스가 사용 불가능하면 호출이 즉시 거부되어 호출자를 보호합니다.
  • 속도 제한: 차단기가 실행을 허용한 후에만 속도 제한기가 적용되어 재시도가 할당된 할당량 내에 머물도록 보장합니다.
  • 제한기 윈도우 내 재시도: 재시도는 속도 제한 윈도우 내에서 발생하며, 각 재시도를 예산된 작업으로 취급합니다. 이는 스트레스 상황에서도 시스템을 안정적으로 유지합니다.
  • 결과 기록: 마지막으로 결과가 관찰 가능성을 위해 기록되고 차단기에 피드백됩니다.

단일 파이프라인이 필요한 이유

개별 데코레이터는 종종 자체 시간 모델, 재시도 로직 및 실패 의미를 가지고 있습니다. 이를 겹쳐 사용하면 예기치 않은 행동이 발생합니다. 실행자는 다음을 강제합니다:

  • 공유 시계
  • 공유 실패 모델
  • 공유 실행 라이프사이클

이는 시스템을 예측 가능하게 하고 이해하기 쉽게 만듭니다.

시계 추상화

시간은 복원력 시스템에서 가장 어려운 의존성입니다. LimitPal은 time.time()에 대한 직접 호출을 플러그인 가능한 시계 인터페이스로 교체합니다:

class Clock(Protocol):
    def now(self) -> float: ...
    def sleep(self, seconds: float) -> None: ...
    async def sleep_async(self, seconds: float) -> None: ...

모든 컴포넌트는 이 시계를 사용하며, 단조(monotonic) 시간을 기반으로 하여 시스템 시계 점프, NTP 조정, 혹은 컨테이너 마이그레이션으로 인한 문제를 방지합니다.

테스트 이점

clock.advance(5.0)  # 실제 대기 없이 5초를 빠르게 전진

테스트가 결정론적이고 빠르게 되며, 몇 분에 달하는 재시도 동작을 즉시 시뮬레이션할 수 있습니다.

서킷 브레이커 아키텍처

브레이커는 상태 머신입니다:

CLOSED → OPEN → HALF_OPEN → CLOSED

정상 작동 (CLOSED)

  • 실패가 발생하면 카운터가 증가합니다.
  • 실패 임계값에 도달하면 브레이커가 OPEN 상태로 전환됩니다.

OPEN 상태

  • 모든 호출이 즉시 실패합니다—재시도 없음—빠른 거절을 제공합니다.

HALF_OPEN 상태

  • 복구 타임아웃이 지나면 제한된 수의 탐색 호출이 허용됩니다.
  • 호출이 성공하면 브레이커가 CLOSED 로 돌아가고, 그렇지 않으면 다시 OPEN 상태가 됩니다.

이 규칙은 복구 후 재시도 폭풍을 방지하고 안정성 조절 장치 역할을 합니다.

지터 (Jitter)와 지수 백오프

지터가 없으면 수천 개의 클라이언트가 동시에 재시도하면서 동기화된 스파이크가 발생해 서비스를 압도할 수 있습니다. 작은 무작위 오프셋을 추가하면 재시도가 시간에 걸쳐 분산됩니다:

  • 지터 없음: 모든 재시도가 t = 1 s에 발생
  • 지터 있음: 재시도가 [0.9 s, 1.1 s] 구간에서 발생

이 무작위성은 큰 안정성 향상을 가져옵니다.

Rate Limiting

Limiters operate per key (e.g., user:123, tenant:acme, ip:10.0.0.1). Each key gets its own bucket, preventing a single bad actor from exhausting the global quota.
제한기는 키별로 작동합니다(예: user:123, tenant:acme, ip:10.0.0.1). 각 키는 자체 버킷을 할당받아 단일 악의적인 사용자가 전체 할당량을 고갈시키는 것을 방지합니다.

Internally this requires:
내부적으로는 다음이 필요합니다:

  • Dynamic bucket allocation
    • 동적 버킷 할당
  • TTL eviction
    • TTL 기반 삭제
  • Bounded memory usage
    • 제한된 메모리 사용
  • Optional LRU trimming
    • 선택적 LRU 정리

동기와 비동기의 동등성

LimitPal은 동기와 비동기 실행을 모두 지원하는 통합 API를 제공합니다:

executor.run(...)          # sync
await executor.run(...)   # async

숨겨진 동작 차이가 없으며, 백그라운드 워커, HTTP 서버, CLI 도구 전반에 걸쳐 동일한 사고 모델을 사용할 수 있습니다.

예정 작업

  • Observability hooks
  • Adaptive rate limiting
  • Redis 백엔드 지원
  • Bulkhead 패턴 구현
  • 인기 프레임워크와의 통합

탄력성은 실행에서 끝나는 것이 아니라; 분산 시스템은 실패하며, 설계된 실패 동작이 필수적이다.

참고 문헌

  • 문서:
  • 소스 코드:

피드백을 환영합니다, 특히 깊은 인프라 도구에 관심이 있는 분들의 의견을 기다립니다.

Back to Blog

관련 글

더 보기 »

Django에서 Idempotent Delete 구현하기

‘Implementing an Idempotent Delete in Django’에 대한 표지 이미지 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3...