(DRF) Throttling

duo2208·2022년 2월 4일
0

Django

목록 보기
21/23
post-thumbnail
🚀 (DRF API Guide) Throttling
최대 호출 제한이 없는 API 접속은 위험합니다. DRF 에서 지원하는 Throttling 설정을 사용하면 최대 호출 횟수를 제한할 수 있습니다. Throttling 을 통해 분당 최대 요청을 제한해 봅시다.

Throttling


Throttle 는 특정 조건 하에 최대 호출 횟수를 결정하는 클래스입니다. 이와 관련된 포맷으로는 Rate, Scope 가 있습니다.

  • Rate : 지정 기간 내에 허용할 최대 호출 횟수.
    • 표기 : {숫자}/{간격}
    • 숫자는 지정 간격내의 최대 요청 제한 횟수.
    • 간격은 횟수를 최기화 하는 시간. 사용할 수 있는 키워드로는 s(초), m(분), h(시), d(일).
  • Scope : 각 Rate 에 대한 별칭 (alias).
  • Throttle : 특정 조건 하에 최대 호출 횟수를 결정하는 로직이 구현된 클래스.
"60/s"		# 분당 최대 60개의 요청
"1000/day"	# 하루 요청 회수 1000개 제한

기본 제공 Throttle

  • AnonRateThrottle

    • 인증 요청에는 제한을 두지 않고, 비인증 요청에는 IP 별로 횟수 제한
    • Throttle 클래스 별로 scope 1개만 지정 가능
    • default scope : anon
  • UserRateThrottle

    • 인증 요청에는 유저별로 횟수를 제한하고, 비인증 요청에는 IP 별로 횟수 제한
    • Throttle 클래스 별로 scope 1개만 지정 가능
    • default scope : user
  • ScopedRateThrottle

    • 인증요청에는 유저별로 횟수를 제한하고, 비인증 요청에는 IP 별로 횟수 제한
    • 여러 APIView 내에 throttle_scope 값을 읽어들여, APIView 별로 다른 scope 적용

비인증 요청 횟수 제한이 아닌, 비인증 요청 거부에 대한 설정은 Throttling 이 아닌 Permission 의 영역 입니다.

Throttle 설정

Throttle 의 default 설정은 다음과 같습니다.

# settings.py

REST_FRAMEWORK = {
	'DEFAULT_THROTTLE_CLASSES': [],
    'DEFAULT_THROTTLE_RATES': {
    	`anon': None
        'user': None
	},
}

전역 설정을 원한다면 settings.py 에서 아래와 같이 원하는 CLASS 와 RATE 를 설정해주면 됩니다.

# settings.py

REST_FRAMEWORK = {
	'DEFAULT_THROTTLE_CLASSES': [
    	'rest_framework.throttling.UserRateThrottle',
    	],
    'DEFAULT_THROTTLE_RATES': {
        'user': '10/day',	# 하루에 10번까지의 요청만 허용. 11번째 요청 거부
	},
}

전역 설정이 아닌 각 APIView 별로 개별 설정을 원한다면 throttle_classes 로 지정해주면 됩니다.

# views.py

from rest_framework.throttling import UserRateThrottle

class PostViewSet(ViewSet):
	throttle_classes = UserRateThrottle

429 Too Many Requests 응답

최대 호출 횟수 제한을 넘길 시 429 Too Many Requests 응답을 반환합니다.

{
    "detail": "Request was throttled. Expected available in 86361 seconds."
}

클라이언트 IP

  • X-Fowarded-For : 프록시나 로드 밸런스를 통해 서버에 접속하는 클라이언트의 IP 주소를 식별하는 표준 헤더.

클라이언트 ip 에서 요청 시, X-Fowarded-For 헤더 이름으로 요청이 들어옵니다.
AWS의 로드밸런스와 같은 요청을 중계하는 서버에서 X-Fowarded-For 를 지원한다면 실제 클라이언트 ip 요청 횟수를 체크할 수 있습니다. X-Forwarded-for 를 지원하지않는다면 REMOTE_ADDR 을 활용합니다.

우선순위 : X-Forwarded-For > REMOTE_ADDR



API 마다 다른 Rate 적용해보기


ScopedRateThrottle 미적용시

views.py 의 throttle_classes 에 각기 필요한 Throttle 을 지정합니다.

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [],
    'DEFAULT_THROTTLE_RATES': {
        'contact': '1000/day',
        'upload': '20/day',
    },
}
# throttles.py

class ContactRateThrottle(UserRateThrottle):
	scope = 'contact'
    
class = UploadRateThrottle(UserRateThrottle):
	scope = 'upload'
# views.py

class ContactListView(APIView):
	throttle_classes = [ContactRateThrottle]
    
class ContactDetailView(APIView):
    throttle_classes = [ContactRateThrottle]

class UploadView(APIView):
    throttle_classes = [UploadRateThrottle]

ScopedRateThrottle 적용시

throttle_classes 를 사용하는 방법은 매번 클래스 이름을 상속시켜 주는 일이 번거롭습니다.
ScopedRateThrottle 을 settings.py 에 적용하여 코드를 간결하게 만듭니다.

# settings.py

REST_FRAMEWORK = {
	# ScopedRateThrottle 적용 
    'DEFAULT_THROTTLE_CLASSES': [rest_framework.throttling.ScopedRateThrottle],
    'DEFAULT_THROTTLE_RATES': {
        'contact': '1000/day',
        'upload': '20/day',
    },
}
# views.py

class ContactListView(APIView):
    throttle_scope = 'contact'

class ContactDetailView(APIView):
    throttle_scope = 'contact'

class UploadView(APIView):
    throttle_scope = 'upload'

유저 별로 Rate 다르게 적용하기

커스텀 Throttle 을 만들기 위해 allow_request 함수를 재정의 합니다.

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_RATES': {
        'premium_user': '1000/day',  # premium 유저는 하루에 1000회 요청 제한
        'light_user': '10/day',      # light 유저는 하루에 10회 요청 제한
    },
}
# throttles.py

from rest_framework.throttling import UserRateThrottle

class PremiumThrottle(UserRateThrottle):
    # 본 Throttle에서는 생성자에서 get_rate가져오는 것은 불필요하므로
    # 생성자 오버로딩을 통해 루틴 제거
    def __init__(self):
        pass

    def allow_request(self, request, view):
        premium_scope = getattr(view, 'premium_scope', None)
        light_scope = getattr(view, 'light_scope', None)

        # Profile모델에 is_premium_user 필드가 있다고 가정
        if request.user.profile.is_premium_user: 
            if not premium_scope:  # premium_scope 미지정 시에는 Throttling제한을 하지않음
                return True
            self.scope = premium_scope
        else:
            if not light_scope:  # light_scope 미지정 시에는 Throttling제한을 하지않음
                return True
            self.scope = light_scope

        self.rate = self.get_rate()
        self.num_requests, self.duration = self.parse_rate(self.rate)

        return super().allow_request(request, view)
# views.py 

from rest_framework import viewsets
from .serializers import PostSerializer
from .throttling import PremiumThrottle
from .models import Post

class PostViewSet(viewsets.ModelViewSet)
	queryset = Post.objects.all()
    serializer_class = PostSerializer

    throttle_classes = [PremiumThrottle]
    premium_scope = 'premium_user'
    light_scope = 'light_user'

    def perform_create(self, serializer):
        print(self.request.FILES)
        serializer.save(author=self.request.user)



Cache


🚀 (Django) Cache

매 요청 시마다 cache 에서 timestamp list 를 get/set 하고 있어 프로젝트가 커질수록 cache 의 성능이 중요해집니다. django 기본의 SimpleRateThrottle 에서는 다음과 같이 default_cache 를 설정하고 있습니다.

# rest_framewor/throttlig.py

from django.core.cache import cache as default_cache

class SimpleRateThrottle(BaseThrottle):
	cache = default_cache

Django의 Cache 지원

django settings 에서는 default 로 로컬 메모리 캐시 를 사용하고 있어, 서버가 재시작되면 cache 가 모두 초기화 됩니다. 로컬 메모리 캐시 이외에도 다양한 캐시를 지원하고 있습니다.

  • Memcached 서버 지원
    • django.core.cache.backends.MemcachedCache
    • django.core.cache.backends.PyLibMCCache
    • Memcached 메모리 기반의 서버이므로, Memcached 가 재시작되면 데이터가 초기화되는 한계가 있다.
  • 데이터베이스 캐시
    • django.core.cache.backends.DatabaseCache
    • io 부하로 인해 비추천.
  • 파일 시스템 캐시
    • django.core.cache.backends.filebased.FileBasedCache
    • 서버가 분산되어 있다면 활용할 수 없으므로 비추천.
  • 로컬 메모리 캐시
    • django.core.cache.backends.LocMemCache
    • 서버가 재시작 되면 캐시가 모두 초기화. django default 세팅이다.
  • 더미 캐시
    • django.core.cache.backends.DummyCache
    • 실제로 캐시를 수행하진 않는다.

redis 를 활용한 캐시
Memcached 와 유사하나 메모리 캐시 뿐만이 아닌, 디스크 동기화에도 싱크를 맞추고 있어 재시작하여도 초기화 되지 않는 장점이 있습니다.


📌 참고 출처

0개의 댓글