본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 04. 11:18

내가 API 호출을 Rate Limit(속도 제한)으로 인해 놓치지 않게 된 방법 (당신도 할 수 있습니다)

요약

API 호출 시 발생하는 429 Rate Limit 에러와 재시도 로직의 실패 문제를 해결하는 방법을 다룹니다. Redis와 asyncio를 활용하여 전역 속도 제한을 관리하고, 지수 백오프를 결합한 분산 속도 제한 방식을 제안합니다.

핵심 포인트

  • 단순 재시도는 버스트 요청을 유발해 문제를 악화시킴
  • 속도 제한기와 재시도 로직을 분리하여 설계해야 함
  • Redis를 활용한 분산 환경에서의 슬라이딩 윈도우 구현 필요
  • asyncio를 통해 블로킹 없는 효율적인 대기 처리 가능

나는 내 앱이 왜 사용자 요청을 계속해서 조용히 누락시키는지 디버깅하며 주말을 보냈다. 로그에는 일정한 패턴이 나타났다: 429 에러가 쏟아져 들어오고, 그 다음 나의 재시도 로직(retry logic)이 상황을 악화시키며, 결국 전체 파이프라인이 멈춰버리는 패턴이었다.

나는 외부 AI API를 통해 사용자가 제출한 텍스트를 분석하는 서비스를 구축하고 있었다. 몇 초마다 API는 429 Too Many Requests 에러를 반환했다. 단순한 지연 시간을 둔 재시도(retry-with-delay) 방식은 문제를 악화시킬 뿐이었다. 내 코드가 결국 재시도를 시도했을 때, 동일한 버스트 제한(burst limit)에 걸리면서 실패의 연쇄 반응(cascade of failures)을 일으켰다.

이 포스트는 내가 그 난장판을 어떻게 해결했는지에 대한 이야기다. 나는 AI API뿐만 아니라 속도 제한(rate-limited)이 걸린 모든 API에 적용할 수 있는 접근 방식을 공유할 것이다. 또한, 이제 내가 외부 서비스와 통신하는 모든 프로젝트에 바로 집어넣는 코드도 포함할 것이다.

문제 (당신의 문제가 아닌, 나의 문제)

내 설정은 간단했다: Python 워커 프로세스가 Redis 큐에서 메시지를 소비하고, AI API(예시로 https://api.interwestinfo.com/v1/analyze라고 부르겠다)를 호출한 뒤, 결과를 저장하는 방식이다. 해당 API는 IP당 분당 50개의 요청을 허용하는 슬라이딩 윈도우(sliding-window) 방식의 속도 제한(rate limit)을 가지고 있었다.

트래픽이 급증했을 때, 나는 429 에러를 맞닥뜨렸다. 나의 초기 해결책은 재시도 전 단순하게 time.sleep(1)을 호출하는 것이었다. 이는 두 가지 문제를 일으켰다:

  1. 바쁜 대기 (Busy waiting) 가 전체 워커를 차단하여 다른 작업들을 지연시켰다.
  2. 버스트 재시도 (Burst retries) 가 sleep 이후에 종종 한꺼번에 몰려들어, 즉시 다음 429 에러를 유발했다.

나는 더 스마트한 무언가가 필요했다.

효과가 없었던 것들

나는 처음에 몇 가지 막다른 길을 시도해 보았다:

  • random wait를 사용한 ThreadPoolExecutor: 버스트 (Burst) 처리는 개선되었지만, 여전히 백프레셔 (Backpressure)가 없었습니다. 만약 API가 10초 동안 다운된다면, 모든 재시도 시도를 다 소진해 버릴 것입니다.
  • wait_exponential을 사용한 tenacity 라이브러리 활용: 개별 함수 호출에는 훌륭하지만, 전역 속도 제한 (Global rate limits)을 처리하지 못했습니다. 각 호출이 독립적으로 재시도되기 때문에, 20개의 동시 호출이 모두 같은 시간에 재시도될 수 있었습니다.
  • 수동 토큰 버킷 (Token bucket) 알고리즘: 간단한 토큰 버킷을 구현했지만, 분산 락 (Distributed lock)이 없었기에 (내 워커들은 로드 밸런서 뒤에 있었습니다), 토큰을 과다하게 소비했습니다.

결국 효과가 있었던 방법: 지수 백오프 (Exponential Backoff)를 결합한 분산 속도 제한 (Distributed Rate Limiting)

내가 최종적으로 선택한 접근 방식은 다음과 같습니다. 이 방식은 Redis를 중앙 코디네이터로 사용하고, 블로킹 (Blocking)을 피하기 위해 asyncio를 사용합니다. 핵심 통찰은 속도 제한기 (Rate limiter)를 재시도 로직 (Retry logic)으로부터 분리하는 것입니다. 속도 제한기는 전역 할당량 (Global quota)을 강제하고, 재시도 로직은 일시적인 실패를 개별적으로 처리합니다.

1단계: Redis 기반 슬라이딩 윈도우 (Sliding Window) 속도 제한기

import time
from typing import Optional
import redis.asyncio as aioredis
...

이 방식은 정렬된 집합 (Sorted set)을 사용하여 요청 타임스탬프를 추적합니다. 만약 제한을 초과하면, 방금 추가한 항목을 제거하고 False를 반환합니다. 그러면 호출자는 대기 (Back off)해야 함을 알게 됩니다.

2단계: 지터 (Jitter)를 포함한 지수 백오프 (Exponential Backoff) 재시도기

import asyncio
import random
from typing import AsyncCallable, TypeVar
...

하나로 합치기

내 워커에서 사용한 방식은 다음과 같습니다:

import asyncio
import aiohttp

...

교훈 및 트레이드오프 (Trade-offs)

  • 중앙 집중식 속도 제한기(Centralized rate limiter)는 지연 시간(Latency)을 추가합니다: 모든 요청마다 Redis 라운드 트립(Round trip)이 발생합니다. 제 경우에는 호출당 약 1ms가 추가되었는데, 이는 수용 가능한 수준이었습니다. 만약 밀리초 미만(Sub-millisecond) 단위의 체크가 필요하다면, 주기적인 동기화를 수행하는 로컬 토큰 버킷(Local token bucket) 방식을 고려해 보세요.
  • 백오프(Backoff) + 속도 제한기(Rate limiter) 조합은 너무 보수적일 수 있습니다: 두 메커니즘을 모두 사용하면 부하가 심할 때 동시성(Concurrency)이 빠르게 떨어집니다. 이는 설계 의도에 따른 것입니다. 계속해서 실패하는 것보다 작업을 큐(Queue)에 쌓아두는 것이 더 낫기 때문입니다.
  • 모든 에러를 재시도(Retry)해서는 안 됩니다: 저는 429와 5xx 에러만 재시도합니다. 429를 제외한 4xx 에러는 절대 재시도하지 마세요.
  • 모든 재시도를 로그로 남기세요: 이제 저는 각 재시도 시도마다 시도 횟수, 지연 시간, 그리고 이유를 로그로 남깁니다. 이는 디버깅 과정에서 큰 도움이 되었습니다.

다음에 다시 만든다면 다르게 할 점

만약 제가 이 시스템을 처음부터 다시 구축한다면, 커스텀 재시도 로직을 건너뛰고 속도 제한에는 aiolimiter를, 재시도에는 tenacity와 같이 검증된 라이브러리를 사용할 것입니다. 이 라이브러리들도 동일한 기능을 수행하지만, 훨씬 더 정교하게 다듬어져 있습니다. 하지만 직접 구현해 봄으로써 미세한 차이들, 특히 지터(Jitter)의 중요성과 재시도 불가능한 에러에 대해 빠르게 실패(Fail fast)해야 할 필요성을 배울 수 있었습니다.

또한, 서킷 브레이커(Circuit breaker) 로직을 추가하겠습니다. 만약 API가 1분 내에 429 에러를 10번 반환한다면, 30초 동안 시도를 완전히 중단하는 방식입니다. 이는 향후 개선 사항입니다.

이제 당신의 차례입니다

외부 API를 다룰 때 속도 제한(Rate limits)은 피할 수 없는 현실입니다. 분산 속도 제한기(Distributed rate limiter)와 지터(Jitter)를 포함한 지수 백오프(Exponential backoff)의 조합은 저에게 수많은 시간을 아껴주었습니다. 하지만 분명 더 나은 패턴들이 존재할 것입니다.

여러분의 설정은 어떤 모습인가요? 토큰 버킷(Token buckets), 슬라이딩 윈도우(Sliding windows), 혹은 다른 방식을 사용하시나요? 여러분이 운영 환경(Production)에서 이를 어떻게 처리하고 있는지 정말 궁금합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0