Rate Limiting#
API Rate Limiting은 시스템의 안정성과 보안을 유지하면서 공정한 리소스 분배를 보장하는 핵심 메커니즘이다.
Rate Limiting은 특정 시간 간격 동안 API에 대한 요청 수를 제한하는 기술이다. 쉽게 말해, 사용자나 클라이언트가 특정 시간 동안 보낼 수 있는 요청의 횟수에 상한선을 두는 것이다.
예를 들어, “1분당 최대 60회 요청” 또는 “1시간당 1000회 요청"과 같은 제한을 설정할 수 있다. 이러한 제한을 초과하면 API는 일반적으로 HTTP 429 상태 코드(“Too Many Requests”)를 반환하며 요청을 거부한다.
Rate Limiting이 필요한 이유#
시스템 보호
과도한 API 호출은 서버에 부하를 주어 성능 저하나 서비스 중단을 초래할 수 있다. Rate Limiting은 이러한 위험을 방지한다.
비용 관리
각 API 호출에는 컴퓨팅 리소스, 데이터베이스 쿼리, 네트워크 대역폭 등의 비용이 발생한다. 요청 수를 제한함으로써 이러한 비용을 예측 가능한 수준으로 유지할 수 있다.
공정한 사용 보장
Rate Limiting은 소수의 사용자가 API 리소스를 독점하는 것을 방지하고, 모든 사용자에게 공정한 접근 기회를 제공한다.
보안 강화
악의적인 사용자의 무차별 대입 공격(brute force attack)이나 서비스 거부 공격(DDoS)을 방지하는 데 도움이 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| # 간단한 Rate Limiting 구현 예시 (Python/Flask)
from flask import Flask, request, jsonify
import time
from collections import defaultdict
app = Flask(__name__)
# 사용자별 요청 추적을 위한 딕셔너리
request_history = defaultdict(list)
RATE_LIMIT = 5 # 1분당 최대 5회 요청
TIME_WINDOW = 60 # 1분(60초)
@app.before_request
def check_rate_limit():
client_ip = request.remote_addr
current_time = time.time()
# 지난 TIME_WINDOW 초 동안의 요청만 유지
request_history[client_ip] = [timestamp for timestamp in request_history[client_ip]
if current_time - timestamp < TIME_WINDOW]
# 현재 요청 수가 제한을 초과하는지 확인
if len(request_history[client_ip]) >= RATE_LIMIT:
return jsonify({"error": "Rate limit exceeded. Try again later."}), 429
# 현재 요청 시간 기록
request_history[client_ip].append(current_time)
@app.route('/api/resource')
def get_resource():
return jsonify({"data": "This is the requested resource"})
if __name__ == '__main__':
app.run(debug=True)
|
주요 Rate Limiting 알고리즘#
다양한 알고리즘이 Rate Limiting을 구현하는 데 사용된다.
각각의 장단점을 이해하는 것이 중요하다.
고정 윈도우(Fixed Window)#
가장 단순한 형태로, 정해진 시간 단위(예: 1시간, 1일)마다 카운터를 리셋한다.
작동 방식:
- 각 시간 윈도우(예: 매시 정각부터 59분까지)에 대해 카운터를 0으로 초기화
- 각 요청마다 카운터 증가
- 카운터가 제한을 초과하면 요청 거부
- 새 윈도우가 시작되면 카운터 리셋
장점:
단점:
- 윈도우 경계에서 트래픽 스파이크가 발생할 수 있음 (예: 윈도우 끝과 다음 윈도우 시작에 집중되는 요청)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Node.js에서의 고정 윈도우 구현
const rateLimits = {};
const WINDOW_SIZE_MS = 60000; // 1분
const MAX_REQUESTS = 100; // 분당 최대 요청
function fixedWindowRateLimit(userId) {
const currentTime = Date.now();
const windowStart = Math.floor(currentTime / WINDOW_SIZE_MS) * WINDOW_SIZE_MS;
// 새 윈도우라면 카운터 초기화
if (!rateLimits[userId] || rateLimits[userId].windowStart !== windowStart) {
rateLimits[userId] = {
windowStart,
count: 0
};
}
// 요청 카운트 증가
rateLimits[userId].count++;
// 제한 확인
return rateLimits[userId].count <= MAX_REQUESTS;
}
|
슬라이딩 윈도우(Sliding Window)#
시간 윈도우가 고정되지 않고 현재 시점에서 일정 시간 이전까지의 요청을 계산한다.
작동 방식:
- 각 요청의 타임스탬프를 저장
- 현재 시점에서 지정된 시간 이전의 요청 수를 계산
- 이 수가 제한을 초과하면 요청 거부
장점:
- 고정 윈도우보다 더 공정하고 균일한 요청 분배
- 윈도우 경계에서의 트래픽 스파이크 문제 해결
단점:
- 각 요청의 타임스탬프를 저장해야 하므로 메모리 사용량이 증가
- 구현이 약간 더 복잡함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| # Python에서의 슬라이딩 윈도우 구현
import time
from collections import deque
class SlidingWindowRateLimiter:
def __init__(self, max_requests, window_size):
self.max_requests = max_requests # 윈도우당 최대 요청 수
self.window_size = window_size # 윈도우 크기(초)
self.requests = {} # 사용자별 요청 타임스탬프 저장
def is_allowed(self, user_id):
current_time = time.time()
# 새 사용자면 초기화
if user_id not in self.requests:
self.requests[user_id] = deque()
# 윈도우 밖의 오래된 요청 제거
while (self.requests[user_id] and
self.requests[user_id][0] < current_time - self.window_size):
self.requests[user_id].popleft()
# 현재 윈도우 내 요청 수 확인
if len(self.requests[user_id]) < self.max_requests:
self.requests[user_id].append(current_time)
return True
return False
|
토큰 버킷(Token Bucket)#
토큰이 일정 속도로 버킷에 추가되고, 각 요청은 토큰을 소비한다.
버킷이 비어있으면 요청이 거부된다.
작동 방식:
- 고정 용량의 버킷에 일정 속도로 토큰 추가
- 각 API 요청은 버킷에서 토큰을 가져옴
- 버킷이 비어있으면 요청이 거부됨
- 버킷은 최대 용량 이상으로 토큰을 저장할 수 없음
장점:
- 짧은 기간 동안의 트래픽 급증(burst)을 허용하면서도 장기적인 속도 제한 가능
- 유연하고 널리 사용되는 알고리즘
단점:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| // Java에서의 토큰 버킷 구현
public class TokenBucket {
private final long capacity; // 버킷 용량
private final double refillRate; // 초당 리필되는 토큰 수
private double tokens; // 현재 토큰 수
private long lastRefillTimestamp; // 마지막 리필 시간
public TokenBucket(long capacity, double refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTimestamp = System.currentTimeMillis();
}
public synchronized boolean tryConsume(int tokensRequired) {
refill();
if (tokens < tokensRequired) {
return false; // 토큰 부족, 요청 거부
}
tokens -= tokensRequired;
return true; // 토큰 소비, 요청 허용
}
private void refill() {
long now = System.currentTimeMillis();
double tokensSinceLastRefill = (now - lastRefillTimestamp) / 1000.0 * refillRate;
tokens = Math.min(capacity, tokens + tokensSinceLastRefill);
lastRefillTimestamp = now;
}
}
|
누수 버킷(Leaky Bucket)#
요청이 버킷에 추가되고 일정한 속도로 처리된다. 버킷이 가득 차면 새 요청이 거부된다.
작동 방식:
- 고정 용량의 버킷에 요청을 추가
- 버킷에서 일정한 속도로 요청을 처리
- 버킷이 가득 차면 새 요청이 거부됨
장점:
- 일정한 처리 속도를 보장하여 백엔드 시스템 보호
- 출력 속도가 일정하여 예측 가능성 제공
단점:
- 토큰 버킷과 달리 트래픽 급증에 덜 유연함
- 긴 대기열이 형성될 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // JavaScript에서의 누수 버킷 구현
class LeakyBucket {
constructor(capacity, leakRate) {
this.capacity = capacity; // 버킷 용량
this.leakRate = leakRate; // 초당 처리율
this.currentLevel = 0; // 현재 버킷 수위
this.lastLeakTime = Date.now();
}
leak() {
const now = Date.now();
const elapsedTime = (now - this.lastLeakTime) / 1000; // 초 단위 경과 시간
const leakedAmount = elapsedTime * this.leakRate;
this.currentLevel = Math.max(0, this.currentLevel - leakedAmount);
this.lastLeakTime = now;
}
addRequest(size = 1) {
this.leak();
if (this.currentLevel + size <= this.capacity) {
this.currentLevel += size;
return true; // 요청 허용
}
return false; // 버킷 가득 참, 요청 거부
}
}
|
Rate Limiting 구현 시 고려사항#
식별자 선택#
Rate Limiting을 적용할 때 어떤 기준으로 요청을 구분할지 결정해야 한다:
- IP 주소: 가장 기본적인 접근법이지만, NAT나 프록시 환경에서는 여러 사용자가 동일한 IP를 공유할 수 있다.
- API 키: 더 정확한 식별이 가능하지만 키 관리가 필요하다.
- 사용자 ID: 로그인한 사용자를 기준으로 제한한다.
- 복합 식별자: 여러 속성을 조합 (예: IP + 엔드포인트)
세부 제한 설정#
다양한 수준에서 Rate Limiting을 적용할 수 있다:
- 글로벌 제한: 전체 API에 대한 총 요청 수 제한
- 사용자/계정별 제한: 각 사용자나 계정에 대한 제한
- 엔드포인트별 제한: 특정 API 엔드포인트에 대한 제한
- 메서드별 제한: GET, POST 등 HTTP 메서드별 다른 제한 적용
분산 시스템에서의 Rate Limiting#
마이크로서비스 아키텍처나 여러 서버가 있는 환경에서는 중앙화된 Rate Limiting 솔루션이 필요하다:
- Redis 활용: 인메모리 데이터 스토어를 사용하여 요청 카운터 관리
- 전용 Rate Limiting 서비스: 모든 요청이 통과하는 중앙 서비스 구현
- API 게이트웨이 활용: 대부분의 API 게이트웨이는 Rate Limiting 기능 제공
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| // Node.js + Redis를 사용한 분산 Rate Limiting 예시
const redis = require('redis');
const { promisify } = require('util');
const client = redis.createClient();
// Redis 명령어를 Promise로 변환
const incrAsync = promisify(client.incr).bind(client);
const expireAsync = promisify(client.expire).bind(client);
const ttlAsync = promisify(client.ttl).bind(client);
async function rateLimit(identifier, limit, windowSec) {
const key = `ratelimit:${identifier}`;
try {
// 카운터 증가
const count = await incrAsync(key);
// 첫 요청이면 만료 시간 설정
if (count === 1) {
await expireAsync(key, windowSec);
}
// 현재 창의 남은 시간 확인
const ttl = await ttlAsync(key);
// 응답 헤더에 포함할 정보
const headers = {
'X-RateLimit-Limit': limit,
'X-RateLimit-Remaining': Math.max(0, limit - count),
'X-RateLimit-Reset': ttl
};
// 제한 초과 여부 확인
if (count > limit) {
return {
limited: true,
headers: {
…headers,
'Retry-After': ttl
}
};
}
return {
limited: false,
headers
};
} catch (err) {
console.error('Rate limiting error:', err);
// 오류 발생 시 요청 허용 (안전 장치)
return { limited: false, headers: {} };
}
}
// 사용 예시
async function handleRequest(req, res) {
const userId = req.user?.id || 'anonymous';
const result = await rateLimit(userId, 100, 3600); // 시간당 100회
// 응답 헤더에 Rate Limit 정보 추가
Object.entries(result.headers).forEach(([key, value]) => {
res.setHeader(key, value);
});
if (result.limited) {
return res.status(429).json({
error: 'Too many requests, please try again later.'
});
}
// 정상 요청 처리 계속…
}
|
사용자 경험 최적화#
Rate Limiting을 구현할 때 사용자 경험도 고려해야 한다:
명확한 오류 메시지#
Rate Limit에 도달했을 때, 사용자에게 다음 정보를 제공하는 것이 좋다:
- 현재 제한
- 남은 요청 수
- 제한이 초기화되는 시간
- 대응 방안 (예: 나중에 다시 시도, 다른 API 키 사용 등)
HTTP 헤더 활용#
표준 Rate Limit HTTP 헤더를 사용하여 클라이언트에게 제한 정보를 전달:
1
2
3
4
| X-RateLimit-Limit: 100
X-RateLimit-Remaining: 75
X-RateLimit-Reset: 1589458812
Retry-After: 120
|
클라이언트 안내#
API 문서에 Rate Limiting 정책을 명확히 설명하고, 클라이언트가 이를 효과적으로 처리하는 방법을 안내한다:
- 지수 백오프(exponential backoff) 구현
- 캐싱 활용
- 배치 요청 사용
- 필수 데이터만 요청
실제 구현 예시: 다양한 환경에서의 Rate Limiting#
API 게이트웨이#
대부분의 API 게이트웨이 솔루션은 기본적인 Rate Limiting 기능을 제공한다:
- AWS API Gateway: 사용량 계획(Usage Plans)을 통한 제한
- Kong: 다양한 Rate Limiting 플러그인 제공
- NGINX:
limit_req_zone
모듈 - Apigee: Traffic Management 정책
웹 프레임워크 미들웨어#
대부분의 웹 프레임워크는 Rate Limiting 미들웨어 또는 라이브러리를 제공:
- Express.js:
express-rate-limit
패키지 - Django:
django-ratelimit
- Rails:
rack-attack
- Spring Boot:
bucket4j-spring-boot-starter
데이터베이스를 활용한 Rate Limiting#
Redis나 Memcached 같은 고성능 키-값 저장소를 사용하여 구현:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| # Python + Redis를 사용한 구현 예시
import redis
import time
class RedisRateLimiter:
def __init__(self, redis_client, limit, window):
"""
Redis 기반 Rate Limiter 초기화
Args:
redis_client: Redis 클라이언트 인스턴스
limit: 윈도우당 최대 요청 수
window: 시간 윈도우(초)
"""
self.redis = redis_client
self.limit = limit
self.window = window
def is_allowed(self, identifier):
"""
주어진 식별자에 대한 요청이 허용되는지 확인
Args:
identifier: 요청 식별자(사용자 ID, IP 등)
Returns:
(allowed, remaining, reset_time) 튜플
"""
current_time = int(time.time())
window_start = current_time - self.window
# 윈도우 키
key = f"ratelimit:{identifier}"
# 파이프라인으로 여러 Redis 명령 실행
pipe = self.redis.pipeline()
# 현재 시간 윈도우 외의 요청 제거
pipe.zremrangebyscore(key, 0, window_start)
# 현재 요청 추가
pipe.zadd(key, {str(current_time): current_time})
# 현재 윈도우 내 요청 수 계산
pipe.zcard(key)
# 키 만료 설정 (윈도우 시간 + 약간의 버퍼)
pipe.expire(key, self.window + 10)
# 명령 실행 및 결과 가져오기
_, _, current_count, _ = pipe.execute()
# 요청이 허용되는지 확인
allowed = current_count <= self.limit
remaining = max(0, self.limit - current_count)
reset_time = current_time + self.window
return (allowed, remaining, reset_time)
|
고급 Rate Limiting 전략#
동적 Rate Limiting#
시스템 부하, 사용자 행동, 시간대 등에 따라 제한을 동적으로 조정:
- 서버 부하 기반: 서버 부하가 높을 때 제한 강화
- 사용자 행동 기반: 의심스러운 패턴 감지 시 제한 강화
- 시간대 기반: 피크 시간대와 비피크 시간대에 다른 제한 적용
차등 Rate Limiting#
모든 사용자나 요청을 동일하게 취급하지 않고 차별화:
- 사용자 등급별: 무료/유료 사용자에 따른 차등 제한
- 요청 중요도별: 중요한 작업과 덜 중요한 작업에 다른 제한 적용
- 리소스 비용별: 리소스 소비가 많은 엔드포인트에 더 엄격한 제한 적용
비용 기반 Rate Limiting#
단순히 요청 수가 아닌 각 요청의 “비용” 또는 리소스 소비량을 고려:
- 각 API 엔드포인트에 비용 값 할당
- 사용자별 “예산” 설정
- 각 요청마다 비용에 따라 예산 차감
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| // Java에서의 비용 기반 Rate Limiting 구현 예시
public class CostBasedRateLimiter {
private final Map<String, TokenBucket> userBuckets = new ConcurrentHashMap<>();
private final Map<String, Integer> endpointCosts;
private final int defaultBucketCapacity;
private final double refillRate;
public CostBasedRateLimiter(int defaultCapacity, double tokensPerSecond) {
this.defaultBucketCapacity = defaultCapacity;
this.refillRate = tokensPerSecond;
// 엔드포인트별 비용 정의 (실제로는 설정 파일 등에서 로드)
this.endpointCosts = Map.of(
"/api/simple-query", 1, // 가벼운 요청
"/api/user-profile", 2, // 중간 요청
"/api/complex-report", 10, // 무거운 요청
"/api/data-export", 25 // 매우 무거운 요청
);
}
public boolean allowRequest(String userId, String endpoint) {
// 사용자별 토큰 버킷 가져오기 (없으면 생성)
TokenBucket bucket = userBuckets.computeIfAbsent(userId,
id -> new TokenBucket(defaultBucketCapacity, refillRate));
// 엔드포인트 비용 계산 (기본값은 1)
int cost = endpointCosts.getOrDefault(endpoint, 1);
// 요청 허용 여부 확인 (비용만큼 토큰 소비)
return bucket.tryConsume(cost);
}
// 토큰 버킷 구현
private static class TokenBucket {
private final double capacity;
private final double refillRate;
private double tokens;
private long lastRefillTimestamp;
public TokenBucket(double capacity, double refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTimestamp = System.currentTimeMillis();
}
public synchronized boolean tryConsume(int tokensRequired) {
refill();
if (tokens < tokensRequired) {
return false;
}
tokens -= tokensRequired;
return true;
}
private void refill() {
long now = System.currentTimeMillis();
double elapsed = (now - lastRefillTimestamp) / 1000.0;
double tokensToAdd = elapsed * refillRate;
if (tokensToAdd > 0) {
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTimestamp = now;
}
}
}
}
|
실제 사례 연구: 주요 API의 Rate Limiting 전략#
GitHub API#
GitHub API는 다양한 수준의 Rate Limiting을 구현:
- 인증되지 않은 요청: 시간당 60회
- 인증된 요청: 시간당 5,000회
- 검색 API: 분당 30회 (인증된 사용자)
- GraphQL API: 시간당 5,000점 (쿼리 복잡성에 따른 포인트 시스템)
이러한 다층적 접근 방식은 서비스의 안정성을 유지하면서도 다양한 사용자 요구를 수용한다.
Twitter API는 계층화된 접근 방식 사용:
- 표준 API: 15분당 450-900개 요청 (엔드포인트별로 다름)
- 검색 API: 15분당 180개 요청
- 미디어 업로드: 3시간당 30MB
이를 통해 Twitter는 플랫폼 부하를 관리하면서도 다양한 애플리케이션을 지원한다.
Stripe API#
Stripe의 접근 방식:
- 라이브 환경: 초당 25-100개 요청 (엔드포인트별 차등)
- 테스트 환경: 더 관대한 제한
- 웹훅 전송: 초당 최대 100개
- 계정 메타데이터 읽기/쓰기: 초당 최대 15개 요청
결제 처리와 같은 중요한 작업에서는 안정성이 특히 중요하므로, Stripe는 각 엔드포인트의 성격에 맞는 Rate Limiting을 적용한다.
모범 사례와 권장 사항#
Rate Limiting을 효과적으로 구현하기 위한 몇 가지 권장 사항:
설계 단계
- 다층적 제한 설계: 글로벌, 서비스별, 엔드포인트별 제한을 조합
- 그레이스풀 디그레이드(Graceful Degradation): 부하가 높을 때 점진적으로 기능 축소
- 제한 초과 시 대안 제공: 비용이 적은 엔드포인트 제안 또는 배치 작업 권장
구현 단계
- 충분한 버퍼 설정: 최대 용량의 70-80%를 목표로 제한 설정
- 재시도 로직 구현: 지수 백오프, 조지터(jitter) 적용
- 캐싱 활용: 동일한 요청에 대한 응답 캐싱으로 요청 수 감소
- 중앙화된 설정 관리: 런타임에 Rate Limit 설정을 조정할 수 있는 기능 구현
운영 단계
- 지속적인 모니터링: 요청 패턴 및 제한 도달 빈도 분석
- 알림 설정: 비정상적인 요청 패턴 감지 시 알림
- 제한 조정: 사용 패턴과 시스템 용량에 따라 주기적으로 제한 조정
- 사용자 피드백 수용: API 소비자의 피드백을 기반으로 제한 정책 개선
용어 정리#
용어 | 설명 |
---|
레이트 리밋(Rate Limit) | 지정된 시간 창(time window) 내에 허용되는 최대 요청 수 |
쓰로틀링(Throttling) | 요청 속도를 늦추거나 제한하는 프로세스 |
시간 창(Time Window) | 레이트 리밋이 적용되는 시간 간격(초, 분, 시간, 일 단위) |
리밋 식별자(Limit Identifier) | 제한이 적용되는 엔티티(IP 주소, API 키, 사용자 ID 등) |
참고 및 출처#
Rate Limiting Rate Limiting은 MSA(Microservices Architecture) 환경에서 시스템 보안과 안정성을 유지하기 위한 핵심 기술로, 과도한 트래픽으로 인한 서비스 장애 방지와 악성 공격 차단을 목표로 한다.
클라이언트/서비스 간 요청 처리량을 제어하는 메커니즘으로, 특히 API 기반 마이크로서비스 통신에서 중요하다.
Rate Limiting은 단순 트래픽 제어를 넘어 마이크로서비스 생태계의 안전벨트 역할을 수행한다.
2025년 현재, 주요 클라우드 제공업체들은 AI 기반 예측 차단 기능을 표준으로 제공하며, 이는 시스템 보안 설계 시 필수 요소로 자리잡았다. 효과적 구현을 위해서는 서비스 특성에 맞는 알고리즘 선택과 지속적 모니터링 체계 수립이 관건이다.
...