base
"갑자기 웹사이트 접속이 느려졌어요!", "앱이 응답을 안 해요!" 개발을 하다 보면 이런 긴급한 연락을 받을 때가 종종 있습니다. 서비스가 인기를 얻어 많은 사용자들이 몰리는 건 좋은 일이지만, 과도한 트래픽이 한꺼번에 몰리면 서비스가 불안정해지거나 심지어 중단될 수 있습니다.
이러한 문제를 미연에 방지하고 서비스 가용성을 유지하기 위한 중요한 기술 중 하나가 바로
제한 횟수를 초과하면 일반적으로 클라이언트에게
혼잡 제어 기법에는
실무에서는 보통 Rate Limiting을 기본으로 적용하고, 좀 더 고급 전략이 필요할 때 Rate Shaping을 선택적으로 사용합니다.
실무에서 자주 사용되는 대표적인 Rate Limiting 알고리즘을 구체적으로 살펴봅니다.
고정된 시간 단위(예: 1분) 내에 특정 수의 요청만 허용하는 방식입니다.
const rateLimitStore = {}
function fixedWindowLimiter(req, res, next) {
const currentMinute = Math.floor(Date.now() / 60000) // 현재 분 단위 시간
rateLimitStore[currentMinute] = (rateLimitStore[currentMinute] || 0) + 1
if (rateLimitStore[currentMinute] > 100) {
return res.status(429).json({ message: 'Too Many Requests' })
}
}
고정 창의 단점을 보완해, 최근 일정 시간 동안의 요청 수를 동적으로 확인하여 제한을 적용합니다.
const WINDOW_SIZE_MS = 60000; // 1분
const MAX_REQUESTS_PER_WINDOW = 100; // 최대 100회
const requestLog = []
function slidingWindowLimiter(req, res, next) {
const now = Date.now()
requestLog.push(now)
// 오래된 기록이 있으면 배열 맨 앞(가장 오래된)부터 제거 (60s 이전 기록 정리)
while (requestLog.length && requestLog[0] < now - WINDOW_SIZE_MS) {
requestLog.shift()
}
// 최근 1분간 요청이 100건 초과하면 제한
if (requestLog.length > MAX_REQUESTS_PER_WINDOW) {
return res.status(429).json({ message: 'Too Many Requests' })
}
}
지정된 속도로 채워지는 토큰을 사용하며 요청 시 토큰을 소모하는 방식입니다.
/**
* capacity: 버킷의 최대 토큰 수
* refillRate: 초당 충전할 토큰 수
*/
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity // 버킷 최대 토큰 수
this.tokens = capacity // 현재 가진 토큰 수 (초기엔 가득)
this.refillRate = refillRate // 초당 충전할 토큰 수
this.lastRefill = Date.now() // 마지막 충전 시각
}
allowRequest() {
const now = Date.now()
// 지난 시간(초) * 초당 충전율 만큼 토큰을 보충
const refill = ((now - this.lastRefill) / 1000) * this.refillRate
// 토큰을 현재값 + 추가량 계산, capacity 이상은 넘지 않도록
this.tokens = Math.min(this.capacity, this.tokens + refill)
this.lastRefill = now
// 토큰이 1개 이상 남아 있으면 하나 소비하고 허용
if (this.tokens >= 1) {
this.tokens -= 1
return true
}
return false
}
}
토큰 버킷과 유사하지만 요청 처리 속도를 일정하게 유지하는 방식입니다.
class LeakyBucket {
constructor(capacity, leakRate) {
this.capacity = capacity // 버킷의 최대 용량(최대 누적 요청 수)
this.water = 0 // 현재 버킷에 담긴 “물”(요청) 양
this.leakRate = leakRate // 초당 흘려보낼(처리할) 요청 수
this.lastChecked = Date.now() // 마지막으로 누수(처리) 계산을 한 시각
}
allowRequest() {
const now = Date.now()
// 지난 시간(초) 동안 흘러나간 물(처리된 요청) 양
const leaked = ((now - this.lastChecked) / 1000) * this.leakRate
// 버킷에서 흘러나간 만큼 물을 빼되, 최소 0 이상 유지
this.water = Math.max(0, this.water - leaked)
this.lastChecked = now
// 새 요청 1단위를 더 담아도 용량(capacity)을 넘지 않으면 허용
if (this.water + 1 <= this.capacity) {
this.water += 1
return true
}
return false
}
}
효과적으로 Rate Limiting을 적용하기 위해서는 다음과 같은 사항을 고려해야 합니다.
API Rate Limiting은 서비스 가용성을 유지하고 시스템 보호를 위한 필수 기술로, Fixed Window, Sliding Window, Token Bucket, Leaky Bucket 같은 다양한 알고리즘으로 구현 가능합니다.
알고리즘은 서비스 특성과 트래픽 패턴에 맞게 선택하며, 분산 환경에서는 Redis와 같은 캐시 시스템을 활용하여 정확한 Rate Limiting 구현이 필수적이라 할 수 있습니다.