DRF 내장 스로틀링 파헤치기
DRF 내장 스로틀링 파헤치기
Django REST Framework 스로틀링의 내부 동작 원리
프로젝트 과정에서 스로틀링 관련 설정이 필요하여 우선적으로 DRF의 내장 스로틀링을 상세하게 분석해보았다.
스로틀링 방식 비교: 고정 윈도우 vs 슬라이딩 윈도우
1. 고정 윈도우 (Fixed Window
) 방식
문제점: 경계 시점에서 버스트 트래픽 발생 가능
1 2 3 4 5 6 7 8 9 10 11 |------- 1분 -------|------- 1분 -------|------- 1분 -------| 09:00:00 ~~~~~~~~ 09:01:00 ~~~~~~~~ 09:02:00 ~~~~~~~~ 09:03:00 | | | ← 이 1분 동안 ← 이 1분 동안 ← 이 1분 동안 최대 10번 허용 최대 10번 허용 최대 10번 허용 # 결과 - 09:00:50 ~ 09:01:00: 10번 요청 (허용) - 09:01:00 ~ 09:01:10: 10번 요청 (허용) → 실제로는 20초 만에 20번 요청이 처리됨!
2. 슬라이딩 윈도우 (
Sliding Window
) 방식장점: 언제든지 정확한 시간 윈도우 내에서만 제한 적용
1 2 3 4 5 6 7 8 9 현재 시점에서 과거 N분을 항상 확인 09:01:30 기준: |←------ 최근 1분 ------| 09:00:30 ~~~~~~~~ 09:01:30 (현재) 09:01:35 기준: |←------ 최근 1분 ------| 09:00:35 ~~~~~~~~ 09:01:35 (현재)
DRF 스로틀링의 핵심: 슬라이딩 윈도우 알고리즘
DRF는 슬라이딩 윈도우(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 # rest_framework/throttling.py의 SimpleRateThrottle 핵심 로직 def allow_request(self, request, view): """ 핵심 스로틀링 로직 - 슬라이딩 윈도우 구현 """ # 1. 캐시에서 이전 요청 기록들을 가져옴 self.key = self.get_cache_key(request, view) self.history = self.cache.get(self.key, []) self.now = self.timer() # 현재 시간 (time.time()) # 2. 만료된 요청 기록들을 제거 (**핵심!**) while self.history and self.history[-1] <= self.now - self.duration: self.history.pop() # 3. 현재 윈도우 내 요청 수가 제한을 초과하는지 확인 if len(self.history) >= self.num_requests: return self.throttle_failure() return self.throttle_success() def throttle_success(self): """ 요청 허용 시 - 현재 요청을 기록에 추가 """ self.history.insert(0, self.now) self.cache.set(self.key, self.history, self.duration) return True
슬라이딩 윈도우가 실제로 어떻게 작동하는가
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 # 시나리오: "10초에 3번" 제한 설정 # T=0초: 첫 번째 요청 history = [1640000000] 현재 윈도우: [1640000000] (1개) → 허용 ✅ # T=3초: 두 번째 요청 history = [1640000003, 1640000000] 현재 윈도우: [1640000003, 1640000000] (2개) → 허용 ✅ # T=5초: 세 번째 요청 history = [1640000005, 1640000003, 1640000000] 현재 윈도우: [1640000005, 1640000003, 1640000000] (3개) → 허용 ✅ # T=7초: 네 번째 요청 시도 1. 만료 검사: - 1640000000이 (1640000007 - 10) = 1639999997보다 큰가? - 1640000000 > 1639999997 → True (아직 10초 안 지남) 2. 현재 요청 수: 3개 >= 3개 → 거부! ❌ # T=12초: 다시 요청 시도 1. 만료 검사: - 1640000000이 (1640000012 - 10) = 1640000002보다 큰가? - 1640000000 > 1640000002 → False (10초 지남!) 2. history.pop() 실행 → [1640000005, 1640000003] 3. 현재 요청 수: 2개 < 3개 → 허용! ✅핵심 포인트:
- 매 요청마다 “현재 시점 기준 과거 10초”를 동적으로 계산
- 윈도우가 시간에 따라 “슬라이딩”하면서 움직임
- 정확한 시간 기반 제어로 버스트 트래픽 방지
DRF 내장 스로틀링 클래스(AnonRate
, UserRate
, ScopedRate
)들의 차이점
1. AnonRateThrottle
vs UserRateThrottle
- AnonRateThrottle:
- 익명 사용자의 API 호출 횟수 제한
- 요청의 IP 주소가 고유한 캐시 키로 사용
- UserRateThrottle:
- 특정 사용자의 API 호출 횟수 제한
- 사용자 ID(인증) / IP 주소(익명)가 고유한 캐시 키로 사용
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 # AnonRateThrottle의 get_cache_key def get_cache_key(self, request, view): if request.user.is_authenticated: return None # 인증된 사용자는 제외 # IP 주소 기반 키 생성 ident = self.get_ident(request) # X-Forwarded-For 또는 REMOTE_ADDR return self.cache_format % { 'scope': self.scope, 'ident': ident } # UserRateThrottle의 get_cache_key def get_cache_key(self, request, view): if request.user.is_authenticated: # 사용자 PK 기반 키 생성 ident = request.user.pk else: # 비인증 사용자는 IP 기반 ident = self.get_ident(request) return self.cache_format % { 'scope': self.scope, 'ident': ident }
2. ScopedRateThrottle의 동적 스코프
- ScopedRateThrottle:
- 동적으로 다양한 API 호출 횟수 제한
- 사용자 ID와 View의 scope를 연결하여 캐시 키로 사용
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class ScopedRateThrottle(UserRateThrottle): def __init__(self): # 동적으로 스코프가 결정됨 pass def allow_request(self, request, view): # 뷰에서 throttle_scope 속성을 가져와서 동적으로 스코프 설정 self.scope = getattr(view, self.scope_attr, None) if not self.scope: return True self.rate = self.get_rate() self.num_requests, self.duration = self.parse_rate(self.rate) return super().allow_request(request, view)
This post is licensed under CC BY 4.0 by the author.