DRF Throttling(요청 제한) 완전 정복: 왜 필요하고, 어떻게 설정·적용·커스텀할까?
nginx 리버스 프록시에서 limit_req로 “입구 컷”을 하는 건 분명 효과적입니다. 하지만 뷰/액션별로 서로 다른 정책(예: 로그인은 분당 5회, 업로드는 하루 20회, 조회 API는 사용자당 1000회)을 걸고 싶을 때는, 서버/인프라가 바뀔 수도 있는 설정에만 기대기 어렵죠. DRF의 throttling은 애플리케이션 레벨에서 ‘엔드포인트 특성’에 맞춘 제한을 만들 수 있다는 점에서 “반드시 알아두면 좋은 기본기”입니다.
Throttling이 필요한 이유
DRF throttling은 “권한(permission)”처럼 요청을 허용할지 말지 결정하지만, 차이는 영구적(권한) vs 임시적(요청 빈도 제한) 입니다. DRF 문서도 throttling을 “클라이언트가 API에 보낼 수 있는 요청 속도를 제어하는 임시 상태”로 설명합니다.
현실적인 필요성은 대략 이렇게 정리됩니다.
- 남용/공격 완화: 무차별 대입(로그인), 스팸성 요청, 크롤링, 단순 DoS 등
- 비용/리소스 보호: 업로드, 외부 API 호출, 무거운 쿼리, 생성형 AI 호출 같은 “비싼” 엔드포인트
- 공정성(fair use): 특정 사용자/키가 자원을 독점하지 않게
- 정책의 코드화: “이 API는 분당 N회” 같은 규칙을 인프라가 아니라 코드로 관리
그리고 중요한 포인트: throttling은 전역(global) 으로도 걸 수 있고, 특정 뷰/액션 단위로도 걸 수 있습니다. (바로 여기서 nginx만으로는 부족해지기 쉽습니다.)
DRF Throttling이 동작하는 방식(핵심 개념만)
1) Rate 문자열
DRF는 보통 "100/day", "60/min" 같은 형태로 제한을 설정합니다.
2) “누구를” 제한할 것인가(클라이언트 식별)
UserRateThrottle: 인증 사용자면 user id 기준, 비인증이면 IP 기준으로 제한AnonRateThrottle: 비인증(anonymous) 요청만 IP 기준으로 제한ScopedRateThrottle: “이 뷰는 uploads 스코프” 같은 scope 단위 정책을 적용
IP 식별은 X-Forwarded-For 또는 REMOTE_ADDR를 사용하며, 프록시 뒤에 있다면 NUM_PROXIES 설정이 중요합니다.
3) 상태 저장은 Cache를 사용
DRF 기본 throttle 구현은 Django cache backend에 카운트를 저장합니다. 단일 프로세스/단일 서버면 기본 LocMemCache로도 동작하지만, 멀티 워커·멀티 레플리카 환경이면 Redis 같은 공유 캐시가 사실상 필수입니다.
전역(Global) 설정: 가장 빠른 시작
settings.py:
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "100/day",
"user": "1000/day",
},
}
이렇게 하면 “기본 정책”이 전체 API에 적용됩니다.
요청이 제한되면 DRF는 기본적으로 HTTP 429 (Too Many Requests) 를 응답합니다.
뷰에 적용하는 법: 엔드포인트별로 다르게 걸기
1) 클래스 기반 뷰(APIView)에서 지정
전역 정책과 별개로 특정 뷰만 throttle을 다르게 줄 수 있습니다.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle
class ExpensiveView(APIView):
throttle_classes = [UserRateThrottle]
def get(self, request):
return Response({"ok": True})
문서에서도 throttle_classes로 뷰 단위 적용을 안내합니다.
2) 함수 기반 뷰(@api_view)에서 지정
from rest_framework.decorators import api_view, throttle_classes
from rest_framework.throttling import UserRateThrottle
from rest_framework.response import Response
@api_view(["GET"])
@throttle_classes([UserRateThrottle])
def ping(request):
return Response({"pong": True})
3) ViewSet의 특정 action에만 적용(@action)
“리스트 조회는 넉넉하게, 특정 POST 액션은 빡세게” 같은 정책에 유용합니다.
from rest_framework.decorators import action
from rest_framework.throttling import UserRateThrottle
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
class ItemViewSet(ViewSet):
@action(detail=True, methods=["post"], throttle_classes=[UserRateThrottle])
def purchase(self, request, pk=None):
return Response({"purchased": pk})
액션에 설정한 throttle은 viewset 레벨 설정보다 우선합니다.
ScopedRateThrottle로 “뷰 특성별” 정책 만들기(강추)
Scoped throttling은 “이 뷰는 uploads, 저 뷰는 login”처럼 의미 있는 이름(scope) 으로 정책을 분리할 수 있어 운영이 깔끔해집니다.
settings.py:
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.ScopedRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"login": "5/min",
"uploads": "20/day",
"search": "60/min",
},
}
뷰에서 scope만 선언:
from rest_framework.views import APIView
from rest_framework.response import Response
class LoginView(APIView):
throttle_scope = "login"
def post(self, request):
return Response({"ok": True})
DRF는 throttle_scope가 있는 뷰에만 ScopedRateThrottle을 적용하고, scope + 사용자 id 또는 IP로 고유 키를 만들어 카운트합니다.
커스텀 Throttle 만들기: “키를 어떻게 잡을지”가 핵심
내장 throttle로도 꽤 해결되지만, 실무에서는 이런 요구가 자주 나옵니다.
- “로그인은 IP + username 조합으로 제한하고 싶다”
- “API Key별로 제한하고 싶다”
- “특정 헤더/테넌트/조직 단위로 제한하고 싶다”
- “캐시를 default가 아니라 다른 캐시(예: redis cluster)로 쓰고 싶다”
1) 가장 흔한 방식: SimpleRateThrottle 상속
get_cache_key()만 잘 정의하면 “무엇을 기준으로 제한할지”를 마음대로 바꿀 수 있습니다.
from rest_framework.throttling import SimpleRateThrottle
class LoginBurstThrottle(SimpleRateThrottle):
scope = "login"
def get_cache_key(self, request, view):
username = (request.data.get("username") or "").lower().strip()
ident = self.get_ident(request) # IP 기반 식별(프록시 설정 영향)
if not username:
return None # username이 없으면 throttle 적용 안 함(원하면 다르게 처리)
return f"throttle_login:{ident}:{username}"
그리고 settings에 scope rate를 등록:
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"path.to.LoginBurstThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"login": "5/min",
},
}
2) 캐시를 다른 것으로 쓰고 싶다면(cache attribute)
DRF 문서에 나온 것처럼, 커스텀 throttle에서 cache를 바꿀 수 있습니다.
from django.core.cache import caches
from rest_framework.throttling import AnonRateThrottle
class CustomCacheAnonThrottle(AnonRateThrottle):
cache = caches["alternate"]
배포하기 전 꼭 알아두자!
1) 프록시 환경에서 IP가 다 똑같이 잡히는 문제
IP 식별은 X-Forwarded-For/REMOTE_ADDR 기반입니다. 프록시 뒤라면 NUM_PROXIES를 정확히 설정하지 않으면 “모든 사용자가 한 명으로” 취급되는 사고가 납니다.
2) LocMemCache는 멀티 워커/멀티 서버에 약하다
캐시가 프로세스 로컬이면 워커마다 카운트가 따로 놀 수 있습니다. 운영에서는 Redis 같은 공유 캐시가 안전합니다(스로틀링이 “제대로” 동작하려면).
3) 동시성 경쟁 조건(race condition)
DRF 내장 구현은 고동시성에서 몇 개 요청이 더 통과할 수 있는 경쟁 조건 가능성이 있다고 문서에 명시돼 있습니다. 정말 “정확히 N회에서 끊어야” 하는 결제/쿠폰 같은 케이스면, 원자적 카운팅(예: Redis INCR + EXPIRE) 기반으로 커스텀 구현을 고려하세요.
4) 클라이언트 친화성: 429와 Retry-After
DRF는 제한 시 기본적으로 429를 반환합니다.
또한 throttle의 wait()를 구현하면 Retry-After 헤더를 포함할 수 있습니다.
마무리: nginx와 DRF throttling, 둘 다 가져가자

- nginx: 대량 트래픽/공격을 가장 앞단에서 깎아내는 방패
- DRF throttling: 엔드포인트의 의미와 비용을 아는 애플리케이션 레벨에서 정교한 정책 적용
특히 “뷰 하나하나 특성에 맞는 제한”은 DRF throttling이 가장 손에 익고, 서버 환경이 바뀌어도 코드로 유지할 수 있어 강력합니다.