Django REST Framework/DRF 일반

[DRF] 공식 문서 - Throttling 정리

bluebamus 2024. 1. 22.

 1. 스로틀링(Throttling)

   - 스로틀링은 요청이 승인되어야 하는지 결정하는 데 있어서 권한과 유사하다. 스로틀은 임시 상태를 나타내며, 클라이언트가 API에 요청할 수 있는 비율을 제어하는 데 사용된다.

   - 권한과 마찬가지로 여러 스로틀을 사용할 수 있다. API에는 인증되지 않은 요청에 대한 제한적인 스로틀과 인증된 요청에 대한 덜 제한적인 스로틀이 있을 수 있다.

   - 특히 리소스를 많이 사용하는 서비스 때문에 API의 다른 부분에 대해 다른 제약을 적용해야 하는 경우, 여러 스로틀을 사용하고 싶을 수 있다.

   - 또한, 폭발적인 스로틀링 비율과 지속적인 스로틀링 비율을 모두 적용하려는 경우에도 여러 스로틀을 사용할 수 있습니다. 예를 들어, 사용자가 분당 최대 60개의 요청과 하루에 최대 1000개의 요청을 할 수 있도록 제한하고 싶을 수 있다.

   - 스로틀은 반드시 요청의 비율 제한을 의미하는 것은 아니다. 예를 들어, 저장 서비스는 대역폭에 대해 스로틀을 사용해야 할 수도 있고, 유료 데이터 서비스는 특정 레코드 접근 수에 대해 스로틀을 사용하고 싶을 수도 있다.

   - **REST 프레임워크가 제공하는 애플리케이션 수준의 스로틀링은 보안 조치 또는 무작위 대입 공격 또는 서비스 거부 공격에 대한 보호로 간주되어서는 안된다. 고의적으로 악의적인 행동을 하는 사람들은 항상 IP 출처를 속일 수 있다. 이에 더해, 내장 스로틀링 구현은 Django의 캐시 프레임워크를 사용하여 구현되며, 요청 비율을 결정하기 위해 비원자적 연산을 사용하는데, 이로 인해 때때로 약간의 불분명함이 발생할 수 있다.

   - REST 프레임워크가 제공하는 애플리케이션 수준의 스로틀링은 다른 비즈니스 계층과 서비스 과용에 대한 기본적인 보호를 구현하는 데 의도되어 있다.**

 

   1) 쓰로틀링 결정 방법(How throttling is determined)

      - 권한 및 인증과 마찬가지로, REST 프레임워크에서의 스로틀링은 항상 클래스의 리스트로 정의된다.

      - 뷰의 주요 부분을 실행하기 전에 리스트의 각 스로틀이 확인된다. 만약 어떤 스로틀 검사가 실패하면 exceptions.Throttled 예외가 발생하고, 뷰의 주요 부분은 실행되지 않는다.

 

   2) 쓰로틀링 정책 설정(Setting the throttling policy)

      - 기본 스로틀링 정책은 DEFAULT_THROTTLE_CLASSES 및 DEFAULT_THROTTLE_RATES 설정을 사용하여 전역적으로 설정할 수 있다.

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day'
    }
}

 

      - DEFAULT_THROTTLE_RATES에서 사용하는 비율은 스로틀 기간으로 초, 분, 시간 또는 일을 포함할 수 있다.

      - 또한 APIView 기반의 뷰를 사용하여 뷰 또는 뷰셋 기반으로 스로틀링 정책을 설정할 수도 있다.

from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView

class ExampleView(APIView):
    throttle_classes = [UserRateThrottle]

    def get(self, request, format=None):
        content = {
            'status': 'request was permitted'
        }
        return Response(content)

 

      - 함수 기반 뷰에 @api_view 데코레이터를 사용하는 경우, 다음 데코레이터를 사용할 수 있다.

@api_view(['GET'])
@throttle_classes([UserRateThrottle])
def example_view(request, format=None):
    content = {
        'status': 'request was permitted'
    }
    return Response(content)

 

      - @action 데코레이터를 사용하여 생성된 경로에 대한 스로틀 클래스를 설정하는 것도 가능하다. 이 방식으로 설정된 스로틀 클래스는 뷰셋 수준의 클래스 설정을 덮어쓴다.

@action(detail=True, methods=["post"], throttle_classes=[UserRateThrottle])
def example_adhoc_method(request, pk=None):
    content = {
        'status': 'request was permitted'
    }
    return Response(content)

 

 

   3) 클라이언트 식별 방법(How clients are identified)

      - 스로틀링을 위해 고유한 클라이언트 IP 주소를 식별하는 데 X-Forwarded-For HTTP 헤더와 REMOTE_ADDR WSGI 변수가 사용된다. X-Forwarded-For 헤더가 존재하면 그것이 사용되며, 그렇지 않으면 WSGI 환경에서의 REMOTE_ADDR 변수 값이 사용된다.

      - 고유한 클라이언트 IP 주소를 엄격하게 식별해야 하는 경우, 먼저 API가 실행되는 애플리케이션 프록시의 수를 NUM_PROXIES 설정을 통해 설정해야 한다. 이 설정은 0 이상의 정수여야 합니다. 0이 아닌 값으로 설정하면, 클라이언트 IP는 X-Forwarded-For 헤더의 마지막 IP 주소로 식별되며, 이때 애플리케이션 프록시 IP 주소가 먼저 제외됩니다. 0으로 설정하면, REMOTE_ADDR 값이 항상 식별 IP 주소로 사용된다.

      - NUM_PROXIES 설정을 구성하면, 고유한 NAT'된 게이트웨이 뒤의 모든 클라이언트가 단일 클라이언트로 취급된다는 것을 이해하는 것이 중요하다.

         - 해당 설명은 NUM_PROXIES 설정을 구성할 경우, 고유한 NAT 게이트웨이 뒤에 있는 모든 클라이언트가 단일 클라이언트로 처리된다는 것을 강조하고 있다.
         - 일반적으로, 클라이언트들은 각자의 고유한 IP 주소를 가지고 있으며, 서버는 이를 통해 각 클라이언트를 식별한다. 하지만 NAT(Network Address Translation) 게이트웨이를 통해 인터넷에 접속하는 여러 클라이언트들은 하나의 공용 IP 주소를 공유한다. 이 경우, 서버는 클라이언트들을 구별하기 어렵게 된다.
         - NUM_PROXIES 설정은 이러한 상황에서 사용되는 설정 값으로, NAT 게이트웨이 뒤에 있는 클라이언트들을 단일 클라이언트로 취급하도록 서버에 지시한다. 이를 통해 서버는 NAT 게이트웨이를 통해 동일한 IP 주소를 공유하는 클라이언트들을 하나의 그룹으로 간주하고, 이 그룹 내에서의 클라이언트들을 식별할 수 있게 된다.
         - 이 설정을 사용하면, 예를 들어 로그인 시도 횟수 제한 등과 같은 보안 기능을 구현할 때, NAT 게이트웨이를 공유하는 클라이언트들이 각자의 제한을 우회하는 것을 방지할 수 있다. 또한, 로그 분석이나 사용자 추적 등의 작업을 보다 정확하게 수행할 수 있다.

      - X-Forwarded-For 헤더가 어떻게 작동하는지에 대한 추가적인 맥락과 원격 클라이언트 IP를 식별하는 방법에 대한 정보는 여기에서 찾을 수 있다.
         - found here : http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster

 

   4) 캐시 설정(Setting up the cache)

      - REST 프레임워크에서 제공하는 스로틀 클래스는 Django의 캐시 백엔드를 사용한다. 적절한 캐시 설정을 설정했는지 확인해야 한다. 기본값인 LocMemCache 백엔드는 간단한 설정에 적합해야 합니다. 자세한 내용은 Django의 캐시 문서를 참조하면 된다.

         - cache documentation : https://docs.djangoproject.com/en/stable/topics/cache/#setting-up-the-cache

      - 'default' 이외의 캐시를 사용해야 하는 경우, 사용자 정의 스로틀 클래스를 생성하고 캐시 속성을 설정하여 이를 수행할 수 있다.

from django.core.cache import caches

class CustomAnonRateThrottle(AnonRateThrottle):
    cache = caches['alternate']

 

      - 'DEFAULT_THROTTLE_CLASSES' 설정 키에 사용자 정의 스로틀 클래스를 설정하거나, throttle_classes 뷰 속성을 사용하여 설정해야 한다.

 

   5) 동시성에 대한 참고 사항(A note on concurrency)

      - 내장 스로틀 구현은 경쟁 조건에 개방되어 있어, 고도의 동시성 하에서 몇몇 추가 요청을 허용할 수 있다.

      - 프로젝트가 동시 요청 중에 요청 수를 보장하는 데 의존하는 경우, 직접 스로틀 클래스를 구현해야 한다. 자세한 내용은 이슈 #5181을 참조하십시오.

         - issue #5181 : https://github.com/encode/django-rest-framework/issues/5181

 

      - 예시 : 

         - 가정하자면, 동시에 수행되는 10개의 요청을 1초 동안 5개까지만 허용하도록 쓰로틀링을 구현해야 한다. 이를 위해 Throttle 클래스를 직접 구현하면 다음과 같다.

import time
from rest_framework.throttling import BaseThrottle

class CustomThrottle(BaseThrottle):
    # 허용되는 최대 요청 수
    request_limit = 5
    # 제한 시간 윈도우
    duration = 1

    def allow_request(self, request, view):
        # 클라이언트 식별 정보를 가져옴 (예: IP 주소)
        identifier = self.get_ident(request)

        # 클라이언트 식별 정보를 기반으로 저장된 요청 수와 시간 정보를 가져옴
        history = self.cache.get(identifier, [])
        
        # 현재 시간과 윈도우 내에서의 요청 수를 계산
        current_time = time.time()
        request_count = sum(1 for timestamp in history if timestamp >= current_time - self.duration)

        # 현재 요청을 history에 추가
        history.append(current_time)
        self.cache.set(identifier, history)

        # 허용되는 최대 요청 수를 초과한 경우 False 반환
        if request_count > self.request_limit:
            return False

        return True

 

      - 위 코드는 가상의 시나리오를 해결하기 위한 CustomThrottle 클래스를 구현한 예시로, 주요 요소는 다음과 같다.
         - request_limit 변수: 허용되는 최대 요청 수를 나타낸다. 이 예시에서는 5로 설정되어 있다.

         - duration 변수: 제한 시간 윈도우를 나타낸다. 이 예시에서는 1초로 설정되어 있다.

         - allow_request() 메서드: 실제 쓰로틀링 로직이 구현된 메서드이다. 클라이언트 식별 정보를 기반으로 해당 클라이언트의 요청 수와 시간 정보를 저장 및 관리한다.

         - get_ident() 메서드: 클라이언트 식별 정보를 가져오는 로직을 구현해야 한다. 이 예시에서는 단순히 IP 주소를 사용하도록 가정한다.

         - cache 객체: 클라이언트의 요청 수와 시간 정보를 저장하기 위한 캐시 객체로, 실제로는 적절한 캐시 백엔드를 사용해야 한다.


      - 이와 같이 CustomThrottle 클래스를 사용하면, 동시에 수행되는 요청 수를 제한하고 경합 조건을 방지할 수 있다. 이 클래스를 DRF의 쓰로틀링 설정에 추가하여 사용하면 된다.

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'path.to.CustomThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'custom_throttle': '10/second',
    }
}

 

      - 위 예시에서는 DEFAULT_THROTTLE_CLASSES에 CustomThrottle 클래스의 경로를 추가하여 사용하고 있다. 또한, DEFAULT_THROTTLE_RATES를 통해 custom_throttle 이름으로 1초에 10개의 요청을 허용하도록 설정하였다.

      - 이렇게 구현된 CustomThrottle 클래스를 통해 동시성 환경에서 요청 수를 제한하고 경합 조건을 방지할 수 있다.

 

 2. API Reference

   1) 익명 사용자 비율 제한(AnonRateThrottle)

      - AnonRateThrottle은 인증되지 않은 사용자만 스로틀링한다. 들어오는 요청의 IP 주소가 스로틀링 대상의 고유한 키를 생성하는 데 사용된다.

      - 허용된 요청 비율은 다음 중 하나로 결정된다(선호 순서대로).
         - 클래스의 rate 속성, AnonRateThrottle를 오버라이딩하고 속성을 설정하여 제공될 수 있다.
         - DEFAULT_THROTTLE_RATES['anon'] 설정.


      - AnonRateThrottle은 알려지지 않은 출처에서의 요청 비율을 제한하려는 경우에 적합하다.

 

   2) 사용자 비율 제한(UserRateThrottle)

      - UserRateThrottle은 API 전체에서 사용자의 요청 비율을 제한한다. 사용자 ID는 제한에 대한 고유한 키를 생성하는 데 사용된다. 인증되지 않은 요청은 들어오는 요청의 IP 주소를 사용하여 제한에 대한 고유한 키를 생성하는 데 사용된다.

      - 허용된 요청 비율은 다음 중 하나로 결정된다(선호 순서대로).
         - 클래스의 rate 속성, UserRateThrottle을 오버라이딩하고 속성을 설정하여 제공될 수 있다.
         - DEFAULT_THROTTLE_RATES['user'] 설정.


      - API는 동시에 여러 UserRateThrottles를 가질 수 있다. 이를 위해 UserRateThrottle를 오버라이딩하고 각 클래스에 대해 고유한 "scope"를 설정한다.

      - 예를 들어, 다음과 같은 클래스를 사용하여 여러 사용자 스로틀 비율을 구현할 수 있다.

class BurstRateThrottle(UserRateThrottle):
    scope = 'burst'

class SustainedRateThrottle(UserRateThrottle):
    scope = 'sustained'

 

      - 그리고 다음과 같은 설정이 필요하다.

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'example.throttles.BurstRateThrottle',
        'example.throttles.SustainedRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'burst': '60/min',
        'sustained': '1000/day'
    }
}

   

      - UserRateThrottle은 사용자별로 간단한 전역 비율 제한을 원할 경우 적합하다.

 

   3) 범위별 비율 제한(ScopedRateThrottle)

      - ScopedRateThrottle 클래스는 API의 특정 부분에 대한 접근을 제한하는 데 사용할 수 있다. 이 스로틀은 접근 중인 뷰에 .throttle_scope 속성이 포함되어 있는 경우에만 적용된다. 고유한 스로틀 키는 그런 다음 요청의 "scope"와 고유한 사용자 ID 또는 IP 주소를 연결하여 형성된다.

      - 허용된 요청 비율은 요청 "scope"의 키를 사용하여 DEFAULT_THROTTLE_RATES 설정에 의해 결정된다.

      - 예를 들어, 다음과 같은 뷰가 주어졌을 때,

class ContactListView(APIView):
    throttle_scope = 'contacts'
    ...

class ContactDetailView(APIView):
    throttle_scope = 'contacts'
    ...

class UploadView(APIView):
    throttle_scope = 'uploads'
    ...

 

      - 그리고 다음과 같은 설정이 필요하다.

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.ScopedRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'contacts': '1000/day',
        'uploads': '20/day'
    }
}

 

      - 사용자는 ContactListView 또는 ContactDetailView에 대한 요청을 하루에 총 1000번으로 제한한다. UploadView에 대한 사용자 요청은 하루에 20번으로 제한된다.

 

 3. 사용자 정의 쓰로틀(Custom throttles)

   - 사용자 정의 스로틀을 생성하려면 BaseThrottle을 오버라이드하고 .allow_request(self, request, view)를 구현한다. 이 메서드는 요청이 허용되어야 하는 경우 True를, 그렇지 않은 경우 False를 반환해야 한다.

   - 선택적으로 .wait() 메서드도 오버라이드할 수 있다. 구현된 경우, .wait()는 다음 요청을 시도하기 전에 기다려야 할 권장 초(second) 수를 반환하거나 None을 반환해야 한다. .wait() 메서드는 .allow_request()가 이전에 False를 반환한 경우에만 호출된다.

   - .wait() 메서드가 구현되고 요청이 스로틀링된 경우, 응답에 Retry-After 헤더가 포함된다.

 

   1) Example

      - 다음은 매 10회 요청 중 1회를 무작위로 스로틀링하는 비율 스로틀의 예이다.

import random

class RandomRateThrottle(throttling.BaseThrottle):
    def allow_request(self, request, view):
        return random.randint(1, 10) != 1

 

 

 - 공식 사이트 문서 : https://www.django-rest-framework.org/api-guide/throttling/

 

Throttling - Django REST framework

 

www.django-rest-framework.org

 

댓글