[DRF] Throttling

강민성·2024년 9월 26일

DRF API Guide

목록 보기
16/28

스로틀링(Throttling)

HTTP/1.1 420 Enhance Your Calm
- 트위터 API의 비율 제한 응답

스로틀링은 권한 부여 여부를 결정한다는 점에서 권한과 유사합니다. 스로틀링은 일시적인 상태를 나타내며, 클라이언트가 API에 요청할 수 있는 빈도를 제어하는 데 사용됩니다.

권한과 마찬가지로, 여러 개의 스로틀링을 사용할 수 있습니다. 예를 들어, 인증되지 않은 요청에 대해서는 제한적인 스로틀을, 인증된 요청에 대해서는 덜 제한적인 스로틀을 설정할 수 있습니다.

또한, API의 일부 서비스가 특히 리소스를 많이 사용하는 경우, 특정 API 부분에 대해 서로 다른 제한을 적용하기 위해 여러 개의 스로틀링을 사용하는 시나리오가 있을 수 있습니다.

다중 스로틀링은 버스트(burst) 스로틀링과 지속적 스로틀링을 동시에 적용하려는 경우에도 사용됩니다. 예를 들어, 사용자가 분당 최대 60개의 요청과 하루 최대 1,000개의 요청을 보낼 수 있도록 제한하고자 할 수 있습니다.

스로틀링은 반드시 요청 비율 제한만을 의미하는 것은 아닙니다. 예를 들어, 저장소 서비스는 대역폭에 대한 스로틀링이 필요할 수 있고, 유료 데이터 서비스는 일정 수의 기록에 대한 접근을 제한할 수 있습니다.

REST 프레임워크가 제공하는 애플리케이션 수준의 스로틀링은 보안 조치나 brute-force 공격 또는 서비스 거부 공격에 대한 보호 수단으로 간주되어서는 안 됩니다. 악의적인 행위자는 항상 IP 원본을 위조할 수 있습니다. 또한, 내장된 스로틀링 구현은 Django의 캐시 프레임워크를 사용하며, 비원자적(non-atomic) 연산을 사용하여 요청 비율을 결정하므로 약간의 불확실성이 발생할 수 있습니다.

REST 프레임워크의 애플리케이션 수준 스로틀링은 서로 다른 비즈니스 계층에 대한 정책을 구현하거나 서비스 과사용에 대한 기본적인 보호 수단을 제공하는 데 사용됩니다.


스로틀링이 결정되는 방식

권한과 인증과 마찬가지로, REST 프레임워크에서의 스로틀링은 항상 클래스 목록으로 정의됩니다.

뷰의 본체가 실행되기 전에, 목록에 있는 모든 스로틀이 확인됩니다. 어떤 스로틀이라도 실패하면 exceptions.Throttled 예외가 발생하고, 뷰의 본체는 실행되지 않습니다.


스로틀링 정책 설정

기본 스로틀링 정책은 DEFAULT_THROTTLE_CLASSESDEFAULT_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에서 사용되는 속도 제한은 초(second), 분(minute), 시간(hour), 또는 일(day)을 스로틀 기간으로 포함할 수 있습니다.

클래스 기반 뷰인 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)

클라이언트가 식별되는 방식

X-Forwarded-For HTTP 헤더와 REMOTE_ADDR WSGI 변수를 사용하여 클라이언트 IP 주소를 고유하게 식별하여 스로틀링을 수행합니다. X-Forwarded-For 헤더가 존재하면 해당 헤더가 사용되고, 그렇지 않으면 WSGI 환경의 REMOTE_ADDR 값이 사용됩니다.

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

X-Forwarded-For 헤더의 작동 방식 및 원격 클라이언트 IP를 식별하는 방법에 대한 추가 정보는 여기에서 확인할 수 있습니다.


캐시 설정

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

기본 캐시가 아닌 다른 캐시를 사용해야 하는 경우, 커스텀 스로틀 클래스를 생성하고 cache 속성을 설정할 수 있습니다. 예시입니다.

from django.core.cache import caches

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

DEFAULT_THROTTLE_CLASSES 설정 키나 throttle_classes 뷰 속성을 사용하여 커스텀 스로틀 클래스를 설정하는 것을 잊지 마세요.


동시성에 대한 주의

내장된 스로틀 구현은 경합 상태에 취약하므로, 높은 동시성 환경에서는 몇 가지 추가 요청이 통과될 수 있습니다.

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

API 레퍼런스

AnonRateThrottle (익명 사용자 속도 제한)

AnonRateThrottle은 인증되지 않은 사용자에게만 속도 제한을 적용합니다. 들어오는 요청의 IP 주소를 사용하여 속도 제한에 사용할 고유 키를 생성합니다.

허용된 요청 속도는 다음 중 하나에 따라 결정됩니다(우선 순위에 따라 적용).
1. 클래스의 rate 속성: AnonRateThrottle을 재정의하고 이 속성을 설정하여 제공할 수 있습니다.
2. DEFAULT_THROTTLE_RATES['anon'] 설정 값.

AnonRateThrottle은 신원이 확인되지 않은 소스에서 오는 요청의 속도를 제한하고 싶을 때 적합합니다.


UserRateThrottle (사용자 속도 제한)

UserRateThrottle은 사용자당 API 요청 속도를 제한합니다. 사용자 ID를 사용하여 고유 키를 생성하여 속도 제한을 적용합니다. 인증되지 않은 요청은 IP 주소를 사용하여 고유 키를 생성하여 속도 제한을 적용합니다.

허용된 요청 속도는 다음 중 하나에 따라 결정됩니다(우선 순위에 따라 적용).
1. 클래스의 rate 속성: UserRateThrottle을 재정의하고 이 속성을 설정하여 제공할 수 있습니다.
2. DEFAULT_THROTTLE_RATES['user'] 설정 값.

API에는 여러 UserRateThrottle을 동시에 사용할 수 있습니다. 이를 위해서는 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',  # 1분당 60회
        'sustained': '1000/day'  # 하루에 1000회
    }
}

UserRateThrottle은 사용자당 단순한 전역 속도 제한을 구현하고자 할 때 적합합니다.


ScopedRateThrottle (스코프 기반 속도 제한)

ScopedRateThrottle 클래스는 API의 특정 부분에 대한 접근을 제한할 때 사용됩니다. 이 속도 제한은 요청 중인 뷰에 .throttle_scope 속성이 포함된 경우에만 적용됩니다. 그 후 요청 "scope"와 사용자 ID 또는 IP 주소를 결합하여 고유한 속도 제한 키가 생성됩니다.

허용된 요청 속도는 DEFAULT_THROTTLE_RATES 설정에서 요청 "scope"의 키에 따라 결정됩니다.

예를 들어, 다음과 같은 뷰가 있을 때:

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',  # 하루에 1000회
        'uploads': '20/day'  # 하루에 20회
    }
}

이 경우, ContactListViewContactDetailView에 대한 사용자의 요청은 하루에 총 1000번으로 제한됩니다. UploadView에 대한 사용자의 요청은 하루에 20번으로 제한됩니다.


커스텀 스로틀링

사용자 정의 스로틀링을 만들려면 BaseThrottle을 재정의하고 .allow_request(self, request, view) 메서드를 구현하세요. 이 메서드는 요청이 허용될 경우 True를 반환하고, 그렇지 않은 경우 False를 반환해야 합니다.

선택적으로 .wait() 메서드도 재정의할 수 있습니다. 구현된 경우, .wait()는 다음 요청을 시도하기 전에 대기해야 할 시간을 초 단위로 반환하거나 None을 반환해야 합니다. .wait() 메서드는 .allow_request()가 이미 False를 반환한 경우에만 호출됩니다.

.wait() 메서드가 구현되었고 요청이 제한되면 응답에 Retry-After 헤더가 포함됩니다.

예시

다음은 10개의 요청 중 1개를 임의로 제한하는 스로틀링의 예입니다.

import random

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

Reference

DRF API Guide - Throttling

profile
Back-end Junior Developer

0개의 댓글