
AI는 코드를, 나는 판단을
'가상면접사례로 배우는 대규모 시스템 설계 기초' 책에서 Rate Limiter 챕터를 읽고 직접 구현해보고 싶어서 Cursor를 활용해 실습해보았습니다.
처음에는 이런 식으로 요청했습니다:
"ratelimiter를 구현하려고 하는데,
Redis와 다양한 알고리즘을 활용해 애플리케이션 레벨에서 Rate Limiter를 구현하는 프로젝트 입니다.
사용 기술 스택 (예정)
- Kotlin
- Spring Boot
- Spring Data Redis
- Spring Annotation 기반 AOP
이런식으로 구현하려고 해, 근데 ratelimiter에도 다양한 알고리즘이 있잖아 한 두~세개정도 짜려고 하는데 (토큰 버킷은 무조건 포함) 구현해줘"
결과적으로 AI가 5분 만에 구현과 테스트 코드를 모두 완성해줬을 뿐만 아니라, 중간에 발생한 트러블슈팅까지 모두 해결해주었습니다.
이 글에서는 Redis + Lua 기반 3가지 Rate Limiting 알고리즘을 정리하고, AI 시대에 개발자가 집중해야 할 핵심 영역에 대해 공유하겠습니다.
| 알고리즘 | 특징 | 장점 | 단점 |
|---|---|---|---|
| Token Bucket | 토큰을 일정 주기로 충전, 요청 시 소비 | 유연한 트래픽 제어, 버스트 허용 | 구현 복잡도 상승 |
| Fixed Window | 고정 시간 윈도우 내 요청 수 제한 | 구현 간단, 메모리 효율적 | 윈도우 경계에서 스파이크 발생 가능 |
| Sliding Window | 각 요청 시간을 로그로 저장 후 계산 | 정확한 제어 가능 | 메모리 사용량 증가 |
local bucket_key = KEYS[1]
local now = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local refill_rate = tonumber(ARGV[3])
local bucket_data = redis.call('HMGET', bucket_key, 'tokens', 'last_refill')
local current_tokens = tonumber(bucket_data[1]) or capacity
local last_refill = tonumber(bucket_data[2]) or now
-- 토큰 충전 계산
local time_passed = now - last_refill
local tokens_to_add = time_passed * refill_rate
current_tokens = math.min(capacity, current_tokens + tokens_to_add)
-- 요청 허용/거부 판단
if current_tokens >= 1 then
current_tokens = current_tokens - 1
redis.call('HMSET', bucket_key, 'tokens', current_tokens, 'last_refill', now)
redis.call('EXPIRE', bucket_key, 3600)
return 1
else
return 0
end
핵심 로직:
refill_rate만큼 토큰 충전override fun isAllowed(key: String, limit: Int, windowSeconds: Long): Boolean {
val windowKey = "fixed_window:$key"
val now = Instant.now().epochSecond
val windowStart = (now / windowSeconds) * windowSeconds
val script = """
local window_key = KEYS[1]
local limit = tonumber(ARGV[1])
local window_seconds = tonumber(ARGV[2])
local current_count = tonumber(redis.call('GET', window_key)) or 0
if current_count < limit then
redis.call('INCR', window_key)
redis.call('EXPIRE', window_key, window_seconds)
return 1
else
return 0
end
""".trimIndent()
return executeScript(script, windowKey, limit, windowSeconds) == 1L
}
특징:
local log_key = KEYS[1]
local now = tonumber(ARGV[1])
local window_start = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local window_seconds = tonumber(ARGV[4])
-- 윈도우 밖 요청 로그 제거
redis.call('ZREMRANGEBYSCORE', log_key, 0, window_start)
-- 현재 윈도우 내 요청 수 확인
local current_count = redis.call('ZCARD', log_key)
if current_count < limit then
redis.call('ZADD', log_key, now, now .. ':' .. math.random())
redis.call('EXPIRE', log_key, window_seconds * 2)
return 1
else
return 0
end
특징:
ZADD로 각 요청 타임스탬프 기록ZREMRANGEBYSCORE로 만료된 요청 제거Sliding Window Log 부하 테스트 (10초간 동시 요청)
총 요청 수: 100
성공 요청 수: 10 (제한: 10req/min)
실패 요청 수: 90
성공률: 10%
평균 응답 시간: 48ms
알고리즘별 특성 비교:
| AI가 담당한 영역 | 개발자가 집중해야 할 영역 |
|---|---|
| 3가지 알고리즘 코드 구현 | 어떤 알고리즘이 우리 서비스에 적합한가? |
| Lua 스크립트 자동 생성 | Redis 장애 시 대응 정책은? |
| 단위/통합 테스트 코드 작성 | 트래픽 패턴에 따른 임계값 설정 |
| 어노테이션 기반 인터페이스 | 팀 내 도입을 위한 설득과 가이드 |
# redis.conf 핵심 설정
maxmemory-policy: allkeys-lru
timeout: 300
tcp-keepalive: 60
// Circuit Breaker와 결합
@CircuitBreaker(name = "rate-limiter", fallbackMethod = "fallbackAllow")
fun checkRateLimit(key: String): Boolean {
return rateLimiter.isAllowed(key, limit, window)
}
// Redis 장애 시 기본 허용
fun fallbackAllow(key: String, ex: Exception): Boolean = true
알고리즘 선택 기준
운영 모니터링 포인트
확장 고려사항
AI가 대부분의 구현을 도와주는 시대에, 개발자는 "무엇을 만들 것인가"와 "어떻게 운영할 것인가"에 더 집중해야 합니다.
이번 Rate Limiter 구현 경험을 통해 기술 선택의 판단력과 시스템 설계 역량이 개발자의 핵심 가치임을 다시 한번 확인했습니다.
전체 소스코드: GitHub Repository
질문이나 제안: 댓글이나 Issues로 자유롭게 남겨주세요!
#Redis #RateLimiter #Lua #트래픽제어 #시스템설계