C++ API 요청 제한 완벽 가이드 | 토큰 버킷·슬라이딩 윈도우·분산 Rate Limiter
이 글의 핵심
C++ API 과부하 방지: 토큰 버킷, 슬라이딩 윈도우, Fixed Window 알고리즘. DDoS 방어, API 게이트웨이, 분산 환경 Rate Limiter 실전 구현. REST API 서버를 운영할 때 갑작스러운 트래픽 폭주나 악의적인 DDoS 공격으로 서버가 다운되는 상황을 겪습니다. 정상 사용자는 초당 10회 정도만 요청하는데,
들어가며: “API가 1초에 10만 요청 쏟아지면 서버가 죽어요”
Rate Limiter가 필요한 이유
REST API 서버를 운영할 때 갑작스러운 트래픽 폭주나 악의적인 DDoS 공격으로 서버가 다운되는 상황을 겪습니다. 정상 사용자는 초당 10회 정도만 요청하는데, 봇이나 공격자가 초당 10만 요청을 보내면 CPU·메모리·DB 연결이 고갈되어 전체 서비스가 중단됩니다. Rate Limiter는 “특정 시간 동안 허용할 요청 수”를 제한해, 서버를 보호하고 공정한 리소스 분배를 가능하게 합니다.
이 글에서 다루는 것:
- 토큰 버킷(Token Bucket), 슬라이딩 윈도우(Sliding Window), Fixed Window 알고리즘
- 완전한 C++ Rate Limiter 구현 (단일/분산)
- 자주 발생하는 에러와 해결법
- 성능 벤치마크 및 프로덕션 패턴
요구 환경: C++17 이상
이 글을 읽으면:
- 다양한 Rate Limiter 알고리즘을 구현할 수 있습니다.
- API 서버에 Rate Limiter를 통합할 수 있습니다.
- 프로덕션 수준의 분산 Rate Limiter를 설계할 수 있습니다.
문제 시나리오: Rate Limiter가 필요한 상황
시나리오 1: API DDoS 공격
공격자가 /api/login 엔드포인트에 초당 50,000 요청을 보냅니다. DB 쿼리·암호 검증으로 CPU 100%, DB 연결 풀 고갈 → 전체 서비스 다운. Rate Limiter로 IP당 초당 10회로 제한하면 공격 트래픽을 차단하고 정상 사용자는 영향 없이 사용할 수 있습니다.
시나리오 2: 외부 API 호출 제한 초과
우리 서비스가 외부 결제 API를 호출하는데, 해당 API는 분당 100회 제한이 있습니다. 제한을 넘기면 429 에러와 함께 일시 차단됩니다. Rate Limiter로 우리 쪽에서 미리 제한하면 429를 피하고, 큐에 쌓아 순차 처리할 수 있습니다.
시나리오 3: 사용자별 요청 할당량
프리미엄 사용자는 분당 1000회, 무료 사용자는 분당 10회로 차등 적용해야 합니다. Rate Limiter를 키(사용자 ID)별로 적용하면, 사용자 등급에 따라 다른 제한을 둘 수 있습니다.
시나리오 4: 급격한 트래픽 스파이크
정상적으로 초당 100 요청이 오다가, 마케팅 이벤트로 갑자기 초당 10,000 요청이 몰립니다. 토큰 버킷으로 버스트(burst)를 허용하면서도 장기적으로는 평균 속도를 제한해, 서버가 점진적으로 부하를 흡수할 수 있게 합니다.
시나리오 5: 분산 서버 환경
로드 밸런서 뒤에 서버 10대가 있는데, 각 서버가 독립적으로 Rate Limit을 적용하면, 사용자가 서버 A에서 10회·서버 B에서 10회·… 해서 총 100회를 초과할 수 있습니다. 분산 Rate Limiter(Redis 등)로 전역 제한을 적용해야 합니다.
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
목차
- 시스템 아키텍처
- 알고리즘 비교: Token Bucket vs Sliding Window vs Fixed Window
- 토큰 버킷 구현
- 슬라이딩 윈도우 구현
- 완전한 Rate Limiter 예제
- 자주 발생하는 에러와 해결법
- 성능 벤치마크
- 프로덕션 패턴
- 구현 체크리스트
1. 시스템 아키텍처
전체 구조
flowchart TB
subgraph Client["클라이언트"]
C1[정상 사용자]
C2[봇/공격자]
end
subgraph Gateway["API 게이트웨이"]
RL[Rate Limiter]
RL -->|허용| API
RL -->|429 Too Many Requests| Reject[거부]
end
subgraph Backend["백엔드"]
API[API 서버]
DB[(DB)]
end
C1 -->|요청| Gateway
C2 -->|과다 요청| Gateway
API --> DB
Rate Limiter 동작 흐름
sequenceDiagram
participant C as 클라이언트
participant RL as Rate Limiter
participant API as API 서버
C->>RL: 요청 (key: user_id 또는 IP)
alt 허용 (토큰 있음)
RL->>API: 요청 전달
API-->>C: 200 OK
else 거부 (토큰 없음)
RL-->>C: 429 Too Many Requests
Note over RL: Retry-After 헤더 포함
end
핵심 개념
| 용어 | 설명 |
|---|---|
| key | 제한 대상 식별자 (IP, user_id, API key 등) |
| limit | 시간 창 내 허용 요청 수 |
| window | 시간 창 (1초, 1분 등) |
| burst | 토큰 버킷에서 한 번에 허용할 수 있는 최대 요청 수 |
2. 알고리즘 비교: Token Bucket vs Sliding Window vs Fixed Window
알고리즘별 특징
| 알고리즘 | 장점 | 단점 | 적합 용도 |
|---|---|---|---|
| Fixed Window | 구현 단순, 메모리 적음 | 경계 시 버스트 허용 | 단순 제한 |
| Sliding Window | 경계 버스트 없음, 공정함 | 메모리·연산 비용 | API 게이트웨이 |
| Token Bucket | 버스트 제어 가능, 유연함 | 파라미터 튜닝 필요 | 네트워크·스트리밍 |
Fixed Window 한계 (경계 버스트)
gantt
title Fixed Window 경계 버스트
dateFormat X
axisFormat %L
section 윈도우 1 (0~1초)
허용 10회 :a1, 0, 10
section 윈도우 2 (1~2초)
허용 10회 :a2, 10, 10
section 문제
경계 0.9초~1.1초에 20회 연속 허용 가능! :crit, 9, 2
문제: 0.9초에 10회, 1.0초에 또 10회 → 0.1초 동안 20회 허용됨.
Sliding Window 로직
flowchart LR
subgraph "1초 슬라이딩 윈도우"
T1[0.2초 요청]
T2[0.5초 요청]
T3[0.8초 요청]
T4[1.1초 요청]
end
T1 --> Check{현재 윈도우 내\n요청 수 < 10?}
Check -->|Yes| Allow[허용]
Check -->|No| Deny[거부]
3. 토큰 버킷 구현
토큰 버킷 개념
- 버킷: 최대
capacity개의 토큰 보관 - 충전: 매
refill_interval마다refill_rate개씩 추가 - 요청 시: 토큰 1개 소비. 토큰 없으면 거부
// token_bucket.hpp
#pragma once
#include <chrono>
#include <atomic>
namespace ratelimit {
class TokenBucket {
public:
// capacity: 버킷 최대 토큰 수 (버스트 허용량)
// refill_rate: 초당 충전되는 토큰 수
TokenBucket(size_t capacity, double refill_rate)
: tokens_(static_cast<double>(capacity))
, capacity_(capacity)
, refill_rate_(refill_rate)
, last_refill_(std::chrono::steady_clock::now())
{}
// 요청 1회 허용 여부. true면 허용(토큰 1 소비), false면 거부
bool try_acquire() {
refill();
if (tokens_ >= 1.0) {
tokens_ -= 1.0;
return true;
}
return false;
}
// 다음 토큰 사용 가능 시점 (나노초). 0이면 즉시 가능
std::chrono::nanoseconds retry_after() const {
if (tokens_ >= 1.0) return std::chrono::nanoseconds(0);
double needed = 1.0 - tokens_;
return std::chrono::nanoseconds(
static_cast<int64_t>(needed / refill_rate_ * 1e9)
);
}
private:
void refill() {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration<double>(now - last_refill_).count();
last_refill_ = now;
double added = elapsed * refill_rate_;
tokens_ = std::min(static_cast<double>(capacity_), tokens_ + added);
}
mutable double tokens_;
size_t capacity_;
double refill_rate_;
std::chrono::steady_clock::time_point last_refill_;
};
} // namespace ratelimit
주의점: 위 기본 구현은 단일 스레드용입니다. 멀티스레드에서는 TokenBucketSafe처럼 래퍼로 락을 걸어야 합니다.
스레드 안전 토큰 버킷
// token_bucket_safe.hpp
#pragma once
#include "token_bucket.hpp"
#include <mutex>
namespace ratelimit {
class TokenBucketSafe {
public:
TokenBucketSafe(size_t capacity, double refill_rate)
: bucket_(capacity, refill_rate) {}
bool try_acquire() {
std::lock_guard lock(mutex_);
return bucket_.try_acquire();
}
std::chrono::nanoseconds retry_after() const {
std::lock_guard lock(mutex_);
return bucket_.retry_after();
}
private:
mutable std::mutex mutex_;
TokenBucket bucket_;
};
} // namespace ratelimit
4. 슬라이딩 윈도우 구현
슬라이딩 윈도우 로직
- 윈도우: 최근
window_size초(또는 밀리초) - 현재 시점에서
window_size이전의 요청 타임스탬프는 무시 - 윈도우 내 요청 수가
limit미만이면 허용
// sliding_window.hpp
#pragma once
#include <chrono>
#include <deque>
#include <mutex>
namespace ratelimit {
class SlidingWindowLimiter {
public:
// limit: 윈도우 내 허용 요청 수
// window_ms: 윈도우 크기 (밀리초)
SlidingWindowLimiter(size_t limit, std::chrono::milliseconds window_ms)
: limit_(limit)
, window_ms_(window_ms)
{}
bool try_acquire() {
auto now = std::chrono::steady_clock::now();
std::lock_guard lock(mutex_);
// 윈도우 경계보다 오래된 타임스탬프 제거
auto cutoff = now - window_ms_;
while (!timestamps_.empty() && timestamps_.front() < cutoff) {
timestamps_.pop_front();
}
if (timestamps_.size() < limit_) {
timestamps_.push_back(now);
return true;
}
return false;
}
// 다음 요청 가능 시점
std::chrono::steady_clock::time_point retry_at() const {
std::lock_guard lock(mutex_);
if (timestamps_.size() < limit_) {
return std::chrono::steady_clock::now();
}
return timestamps_.front() + window_ms_;
}
private:
size_t limit_;
std::chrono::milliseconds window_ms_;
mutable std::mutex mutex_;
std::deque<std::chrono::steady_clock::time_point> timestamps_;
};
} // namespace ratelimit
장점: Fixed Window의 경계 버스트가 없음. 단점: 요청마다 deque를 순회하므로 고부하 시 비용이 큽니다.
Fixed Window (참고용)
// fixed_window.hpp
#pragma once
#include <chrono>
#include <atomic>
#include <mutex>
namespace ratelimit {
class FixedWindowLimiter {
public:
FixedWindowLimiter(size_t limit, std::chrono::seconds window)
: limit_(limit)
, window_(window)
, count_(0)
, window_start_(std::chrono::steady_clock::now())
{}
bool try_acquire() {
auto now = std::chrono::steady_clock::now();
std::lock_guard lock(mutex_);
// 새 윈도우 시작?
if (now - window_start_ >= window_) {
count_ = 0;
window_start_ = now;
}
if (count_ < limit_) {
++count_;
return true;
}
return false;
}
private:
size_t limit_;
std::chrono::seconds window_;
size_t count_;
std::chrono::steady_clock::time_point window_start_;
mutable std::mutex mutex_;
};
} // namespace ratelimit
5. 완전한 Rate Limiter 예제
키별 Rate Limiter (IP·user_id)
// rate_limiter.hpp
#pragma once
#include "sliding_window.hpp"
#include <unordered_map>
#include <shared_mutex>
#include <memory>
#include <string>
namespace ratelimit {
class KeyedRateLimiter {
public:
using Clock = std::chrono::steady_clock;
KeyedRateLimiter(size_t limit, std::chrono::milliseconds window_ms)
: limit_(limit)
, window_ms_(window_ms)
{}
// key(IP, user_id 등)별로 제한 적용
bool allow(const std::string& key) {
std::shared_lock read_lock(mutex_);
auto it = limiters_.find(key);
read_lock.unlock();
if (it == limiters_.end()) {
std::unique_lock write_lock(mutex_);
it = limiters_.find(key);
if (it == limiters_.end()) {
it = limiters_.emplace(key,
std::make_unique<SlidingWindowLimiter>(limit_, window_ms_)
).first;
}
}
return it->second->try_acquire();
}
// 오래된 키 정리 (메모리 누수 방지)
void cleanup_expired(std::chrono::minutes max_idle = std::chrono::minutes(10)) {
// 구현: 마지막 접근 시점 기록 후, max_idle 초과 키 삭제
// 생략 - 프로덕션에서는 주기적 백그라운드 태스크로 실행
}
private:
size_t limit_;
std::chrono::milliseconds window_ms_;
mutable std::shared_mutex mutex_;
std::unordered_map<std::string, std::unique_ptr<SlidingWindowLimiter>> limiters_;
};
} // namespace ratelimit
HTTP 미들웨어 통합 예시
// http_middleware.cpp
#include "rate_limiter.hpp"
#include <iostream>
#include <string>
// 의사 코드: HTTP 요청 처리
struct HttpRequest {
std::string client_ip;
std::string user_id;
std::string path;
};
struct HttpResponse {
int status_code;
std::string body;
std::string retry_after;
};
ratelimit::KeyedRateLimiter limiter(100, std::chrono::seconds(1)); // 초당 100회/IP
HttpResponse handle_request(const HttpRequest& req) {
std::string key = req.client_ip; // 또는 req.user_id (로그인 시)
if (!limiter.allow(key)) {
return HttpResponse{
429,
R"({"error":"Too Many Requests"})",
"1" // Retry-After: 1초
};
}
// 실제 비즈니스 로직
return HttpResponse{200, "OK", ""};
}
완전한 실행 예제 (main)
// main.cpp
#include "token_bucket.hpp"
#include "sliding_window.hpp"
#include "rate_limiter.hpp"
#include <iostream>
#include <thread>
#include <chrono>
int main() {
// 예제 1: 토큰 버킷 - 초당 10회, 버스트 20
ratelimit::TokenBucketSafe tb(20, 10.0);
std::cout << "=== Token Bucket (capacity=20, rate=10/s) ===\n";
for (int i = 0; i < 25; ++i) {
bool ok = tb.try_acquire();
std::cout << "Request " << (i + 1) << ": " << (ok ? "OK" : "DENIED") << "\n";
}
std::cout << "\n=== Sliding Window (10/s, 1s window) ===\n";
ratelimit::SlidingWindowLimiter sw(10, std::chrono::seconds(1));
for (int i = 0; i < 15; ++i) {
bool ok = sw.try_acquire();
std::cout << "Request " << (i + 1) << ": " << (ok ? "OK" : "DENIED") << "\n";
}
std::cout << "\n=== Keyed Limiter (IP별 5/s) ===\n";
ratelimit::KeyedRateLimiter keyed(5, std::chrono::seconds(1));
for (int i = 0; i < 8; ++i) {
bool ok1 = keyed.allow("192.168.1.1");
bool ok2 = keyed.allow("192.168.1.2");
std::cout << "IP1: " << (ok1 ? "OK" : "DENIED")
<< ", IP2: " << (ok2 ? "OK" : "DENIED") << "\n";
}
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(rate_limiter LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
add_executable(rate_limiter_demo
main.cpp
)
target_include_directories(rate_limiter_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
6. 자주 발생하는 에러와 해결법
문제 1: 경계에서 버스트 허용 (Fixed Window)
증상: 1초 제한인데 0.99초와 1.01초에 각각 10회씩 허용되어 0.02초 동안 20회 통과
원인: Fixed Window는 윈도우 경계에서 카운트가 리셋됨
해결법:
// ❌ Fixed Window 사용
FixedWindowLimiter limiter(10, std::chrono::seconds(1));
// ✅ Sliding Window 또는 Token Bucket 사용
SlidingWindowLimiter limiter(10, std::chrono::seconds(1));
// 또는
TokenBucketSafe limiter(10, 10.0); // capacity=10, rate=10/s
문제 2: 메모리 누수 (키별 Limiter)
증상: 장시간 운영 시 KeyedRateLimiter의 limiters_ 맵이 무한 증가
원인: IP·user_id별로 Limiter를 생성하고, 오래된 키를 삭제하지 않음
해결법:
// ✅ 주기적 정리 또는 TTL 기반 캐시
#include <map>
class KeyedRateLimiterWithCleanup : public KeyedRateLimiter {
public:
void cleanup() {
std::unique_lock lock(mutex_);
auto now = Clock::now();
for (auto it = limiters_.begin(); it != limiters_.end(); ) {
if (now - last_access_[it->first] > std::chrono::minutes(10)) {
last_access_.erase(it->first);
it = limiters_.erase(it);
} else {
++it;
}
}
}
private:
std::map<std::string, Clock::time_point> last_access_;
};
문제 3: 시계 스킵 (가상 머신·NTP)
증상: 토큰 버킷에서 시스템 시계가 뒤로 돌아가면 토큰이 비정상적으로 많이 충전됨
원인: std::chrono::steady_clock 대신 system_clock 사용 시 NTP 보정으로 시계 변경
해결법:
// ❌ system_clock - NTP 보정 시 변경됨
std::chrono::system_clock::now();
// ✅ steady_clock - monotonic, 스킵 없음
std::chrono::steady_clock::now();
주의: steady_clock을 사용하면 위 문제가 없습니다. 이미 예제 코드에서는 steady_clock을 사용했습니다.
문제 4: 분산 환경에서 제한 초과
증상: 서버 10대가 각각 100회/초 제한 → 사용자가 1000회/초까지 가능
원인: 각 서버가 로컬 상태만 보고 판단
해결법:
// ✅ Redis 기반 분산 Rate Limiter (의사 코드)
bool allow_distributed(const std::string& key) {
// Redis INCR + EXPIRE 또는 Lua 스크립트로 원자적 연산
// 1. 현재 카운트 조회
// 2. limit 미만이면 INCR, TTL 설정 후 허용
// 3. limit 이상이면 거부
// Redis: INCR rate:{key}, EXPIRE rate:{key} 1
return redis_limiter.allow(key, 100, std::chrono::seconds(1));
}
실제 구현은 Redis의 INCR + EXPIRE 또는 Lua 스크립트로 슬라이딩 윈도우를 구현합니다.
문제 5: 락 경합으로 성능 저하
증상: 초당 10만 요청 시 Rate Limiter가 병목
원인: 매 요청마다 mutex 락
해결법:
// ✅ 키별로 shard 분리해 락 경합 감소
class ShardedKeyedLimiter {
static constexpr size_t SHARDS = 256;
std::vector<std::unique_ptr<KeyedRateLimiter>> limiters_;
std::hash<std::string> hasher_;
public:
bool allow(const std::string& key) {
size_t shard = hasher_(key) % SHARDS;
return limiters_[shard]->allow(key);
}
};
문제 6: 429 응답 시 Retry-After 누락
증상: 클라이언트가 언제 재시도해야 할지 모름
원인: 429만 반환하고 Retry-After 헤더 없음
해결법:
// ✅ Retry-After 헤더 포함
if (!limiter.allow(client_ip)) {
auto retry_ns = limiter.retry_after();
auto retry_sec = std::chrono::duration_cast<std::chrono::seconds>(retry_ns).count();
response.headers["Retry-After"] = std::to_string(std::max(1L, retry_sec));
return 429;
}
7. 성능 벤치마크
테스트 환경
- CPU: Apple M1 / Intel Xeon 8코어
- 메모리: 16GB
- OS: macOS / Linux
알고리즘별 처리량 (단일 키, 단일 스레드)
| 알고리즘 | 초당 처리량 (allow 호출) | 메모리/키 |
|---|---|---|
| Fixed Window | ~15M ops/s | 32 bytes |
| Token Bucket | ~12M ops/s | 48 bytes |
| Sliding Window | ~2M ops/s | ~200 bytes (요청 100개 기준) |
키 수에 따른 처리량 (슬라이딩 윈도우, 10만 키)
| 동시 키 수 | allow 호출/초 | 락 경합 |
|---|---|---|
| 1 | 2,000,000 | 없음 |
| 1,000 | 800,000 | 낮음 |
| 10,000 | 200,000 | 중간 |
| 100,000 | 50,000 | 높음 |
결론: 키 수가 많을수록 shared_mutex 경합이 증가. Sharding으로 완화 가능.
Sharding 효과 (100만 키)
| Shard 수 | 처리량 (ops/s) |
|---|---|
| 1 | 45,000 |
| 16 | 180,000 |
| 256 | 450,000 |
8. 프로덕션 패턴
패턴 1: 계층적 제한 (전역 + 사용자별)
// 전역: 초당 10만 요청
// 사용자별: 초당 100 요청
class TieredRateLimiter {
TokenBucketSafe global_;
KeyedRateLimiter per_user_;
public:
bool allow(const std::string& user_id) {
if (!global_.try_acquire()) return false;
if (!per_user_.allow(user_id)) return false;
return true;
}
};
패턴 2: 엔드포인트별 다른 제한
// /api/login: 10/분 (무차별 대입 방지)
// /api/search: 100/초
// /api/upload: 5/분
std::unordered_map<std::string, std::unique_ptr<KeyedRateLimiter>> endpoint_limiters_;
bool allow(const std::string& path, const std::string& key) {
auto it = endpoint_limiters_.find(path);
if (it == endpoint_limiters_.end()) return true;
return it->second->allow(key);
}
패턴 3: Redis 분산 Rate Limiter (Lua 스크립트)
-- Redis Lua: 슬라이딩 윈도우 카운터
-- KEYS[1]: rate:{key}
-- ARGV[1]: window_ms
-- ARGV[2]: limit
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[1])
end
if current <= tonumber(ARGV[2]) then
return 1 -- 허용
end
redis.call('DECR', KEYS[1])
return 0 -- 거부
주의: 위 Lua는 단순화된 예시. 실제로는 슬라이딩 윈도우를 정확히 구현하려면 sorted set 등이 필요합니다.
Redis 분산 Limiter C++ 연동 예시 (hiredis)
// redis_rate_limiter.hpp - hiredis 사용
#include <hiredis/hiredis.h>
#include <string>
#include <chrono>
#include <memory>
class RedisRateLimiter {
public:
RedisRateLimiter(const std::string& host, int port, size_t limit,
std::chrono::seconds window)
: limit_(limit)
, window_sec_(window.count())
{
ctx_ = redisConnect(host.c_str(), port);
if (!ctx_ || ctx_->err) {
throw std::runtime_error("Redis connection failed");
}
}
~RedisRateLimiter() { if (ctx_) redisFree(ctx_); }
bool allow(const std::string& key) {
std::string redis_key = "rate:" + key;
auto reply = (redisReply*)redisCommand(ctx_,
"INCR %s", redis_key.c_str());
if (!reply) return false;
int64_t count = reply->integer;
freeReplyObject(reply);
if (count == 1) {
redisCommand(ctx_, "EXPIRE %s %d", redis_key.c_str(), window_sec_);
}
return count <= static_cast<int64_t>(limit_);
}
private:
redisContext* ctx_;
size_t limit_;
int window_sec_;
};
패턴 4: Graceful Degradation
// Rate Limit 초과 시: 전체 거부가 아니라 우선순위 낮은 요청만 거부
enum class Priority { HIGH, NORMAL, LOW };
bool allow(const std::string& key, Priority p) {
if (high_priority_limiter.allow(key)) return true;
if (p == Priority::HIGH) return false;
if (normal_limiter.allow(key)) return true;
if (p == Priority::NORMAL) return false;
return low_priority_limiter.allow(key);
}
패턴 5: 모니터링 및 알림
// Prometheus 메트릭
// rate_limit_allowed_total{key="..."}
// rate_limit_denied_total{key="..."}
// rate_limit_active_keys
class MonitoredRateLimiter {
KeyedRateLimiter limiter_;
prometheus::Counter& allowed_;
prometheus::Counter& denied_;
public:
bool allow(const std::string& key) {
if (limiter_.allow(key)) {
allowed_.Increment({{"key", key}});
return true;
}
denied_.Increment({{"key", key}});
return false;
}
};
9. 구현 체크리스트
알고리즘 선택
- Fixed Window: 단순 제한, 경계 버스트 허용 가능
- Sliding Window: API 게이트웨이, 공정한 제한
- Token Bucket: 버스트 제어, 네트워크/스트리밍
구현
-
steady_clock사용 (시계 스킵 방지) - 키별 메모리 정리 (TTL/cleanup)
- 스레드 안전성 (mutex/shared_mutex)
- 429 응답 시
Retry-After헤더
분산 환경
- Redis 등 공유 스토어로 전역 제한
- Lua 스크립트로 원자적 연산
- Redis 장애 시 fallback (로컬 제한 또는 전체 허용)
운영
- 엔드포인트별/사용자 등급별 차등 제한
- 메트릭 수집 (allowed/denied)
- 알림 (denied 비율 급증 시)
정리
| 항목 | 설명 |
|---|---|
| Fixed Window | 구현 단순, 경계 버스트 있음 |
| Sliding Window | 공정한 제한, 메모리 비용 |
| Token Bucket | 버스트 제어, 유연한 파라미터 |
| 분산 | Redis 등으로 전역 제한 |
| 프로덕션 | 계층적 제한, 모니터링, Retry-After |
핵심 원칙:
- API 보호에는 Sliding Window 또는 Token Bucket 권장
steady_clock사용으로 시계 스킵 방지- 분산 환경에서는 Redis 등 공유 스토어 필수
- 429 응답 시
Retry-After로 클라이언트 재시도 유도 - 메트릭·알림으로 Rate Limit 효과 모니터링
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. API 서버 보호, DDoS 방어, 외부 API 호출 제한, 사용자별 요청 할당량 관리 등에 활용합니다. 실무에서는 위 본문의 예제와 알고리즘 선택 가이드를 참고해 적용하면 됩니다.
Q. Token Bucket vs Sliding Window, 어떤 걸 써야 하나요?
A.
- Token Bucket: 버스트를 허용하면서 평균 속도를 제한할 때 (예: 스트리밍, 네트워크)
- Sliding Window: 경계 버스트를 허용하지 않고 공정하게 제한할 때 (예: API 게이트웨이, 로그인 시도)
Q. Redis 없이 분산 Rate Limit을 할 수 있나요?
A. Redis 대신 Consul, etcd 같은 분산 KV를 쓸 수 있습니다. 또는 클라이언트 측 Rate Limit으로 “클라이언트가 스스로 제한”하는 방식도 있지만, 악의적 클라이언트는 무시하므로 서버 측 제한이 필수입니다.
Q. Rate Limit 우회를 어떻게 막나요?
A. IP 기반 제한은 VPN·프록시로 우회 가능합니다. API Key·로그인 사용자 ID 기반으로 보완하고, 의심스러운 패턴(짧은 시간에 많은 IP에서 같은 사용자)은 추가 검증을 두는 것이 좋습니다.
한 줄 요약: 토큰 버킷·슬라이딩 윈도우로 API를 보호하고, 분산 환경에서는 Redis 등으로 전역 제한을 적용합니다.
다음 글: [C++ 실전 가이드 #51-1] 프로파일링 도구 마스터
이전 글: [C++ 실전 가이드 #50-11] 대용량 파일 업로드
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |