본문으로 건너뛰기
김신건의 로그

[DRF] Pagination + Throttling

· 수정 · 📖 약 1분 · 526자/단어 #drf #pagination #throttling #rate-limit
DRF pagination, PageNumberPagination, LimitOffsetPagination, CursorPagination, DRF throttling, UserRateThrottle, AnonRateThrottle

정의

Pagination = 큰 목록을 페이지 단위로 나눔. Throttling = 요청 빈도 제한. 둘 다 API 안정성의 기본.

Pagination 3종

언제 어느 것을 쓸까

flowchart TD
    Q{Pagination 종류}
    Q --> P["PageNumberPagination<br/>?page=1&page_size=20"]
    Q --> L["LimitOffsetPagination<br/>?limit=20&offset=40"]
    Q --> C["CursorPagination<br/>?cursor=abc123"]
종류목적장점단점
PageNumberPagination특정 페이지 이동 (관리자 UI)직관적, “5페이지로 가기” 지원큰 offset 시 느림, 실시간 데이터에 중복/누락
LimitOffsetPagination임의 offset 필요할 때유연 (?limit=10&offset=137)PageNumber와 같은 문제, offset 커지면 여전히 느림
CursorPagination무한 스크롤, 대규모 실시간 피드일관성, offset 무관 O(log N)임의 페이지 이동 불가, 정렬 필드 필수

왜 큰 offset은 느린가?

SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 100000;

DB는 OFFSET 100000 을 무시하는 게 아니라 100000개 row 를 실제로 훑고 버린다. 페이지가 뒤로 갈수록 응답 시간 선형 증가.

CursorPagination 은 WHERE id > last_seen_id ORDER BY id LIMIT 20 로 인덱스만 타므로 어느 페이지든 O(log N).

PageNumberPagination

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
}
GET /api/users/?page=2

응답:

{
  "count": 100,
  "next": "http://api/users/?page=3",
  "previous": "http://api/users/?page=1",
  "results": [{...}, ...]
}

Custom:

class MyPagination(PageNumberPagination):
    page_size = 20
    page_size_query_param = 'page_size'   # ?page_size=50
    max_page_size = 100

LimitOffsetPagination

class MyPagination(LimitOffsetPagination):
    default_limit = 20
    max_limit = 100
GET /api/users/?limit=20&offset=40

CursorPagination (대규모 권장)

class MyPagination(CursorPagination):
    page_size = 20
    ordering = '-created_at'   # 정렬 필드 (필수)
GET /api/users/?cursor=cD0yMDI2LTA2LTI1

응답:

{
  "next": "http://api/users/?cursor=cD0yMDI2LTA1LTMx",
  "previous": null,
  "results": [{...}]
}

TIP

대규모 데이터 (수십만 row+) = CursorPagination 필수. LIMIT/OFFSET 은 offset 커질수록 DB 부담.

View 별 override

class MyView(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    pagination_class = CursorPagination
    # pagination_class = None   # 비활성

응답 포맷 커스터마이즈

class CustomPagination(PageNumberPagination):
    page_size = 20

    def get_paginated_response(self, data):
        return Response({
            'meta': {
                'total': self.page.paginator.count,
                'page': self.page.number,
                'per_page': self.page_size,
                'total_pages': self.page.paginator.num_pages,
            },
            'links': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link(),
            },
            'data': data,
        })

언제 무엇을?

flowchart TD
    Q{시나리오}
    Q -->|"UI 페이지네이션 (1, 2, 3)"| P["PageNumberPagination"]
    Q -->|"무한 스크롤"| L["LimitOffsetPagination or Cursor"]
    Q -->|"큰 데이터 + 실시간"| C["CursorPagination"]
    Q -->|"관리자 화면"| P
    Q -->|"모바일 앱"| C

Throttling

flowchart LR
    Req[Request] --> Throttle[Throttle class]
    Throttle -->|"허용"| View
    Throttle -->|"429 Too Many Requests"| Reject

내장 Throttle Classes

ClassScope
AnonRateThrottle익명 (IP 기반)
UserRateThrottle인증된 user
ScopedRateThrottleView 별 커스텀

설정

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

Rate 형식: 숫자/기간 (second, minute, hour, day).

ScopedRateThrottle

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.ScopedRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'login': '5/min',
        'signup': '10/hour',
        'ai_generate': '30/day',
    }
}

class LoginView(APIView):
    throttle_scope = 'login'
    def post(self, request): ...

class AIGenerateView(APIView):
    throttle_scope = 'ai_generate'
    def post(self, request): ...

Custom Throttle

from rest_framework.throttling import SimpleRateThrottle

class BurstRateThrottle(SimpleRateThrottle):
    scope = 'burst'
    rate = '10/minute'   # 짧은 시간

    def get_cache_key(self, request, view):
        if request.user.is_authenticated:
            ident = request.user.pk
        else:
            ident = self.get_ident(request)
        return f'throttle_burst_{ident}'


class SustainedRateThrottle(SimpleRateThrottle):
    scope = 'sustained'
    rate = '1000/day'

    def get_cache_key(self, request, view):
        return f'throttle_sustained_{request.user.pk}'

429 응답

HTTP/1.1 429 Too Many Requests
Retry-After: 3600

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

TIP

클라이언트는 Retry-After 확인 후 재시도. 자세한 건 retry-with-backoff.

Cache Backend

Throttle 은 Django cache 사용:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://localhost:6379/1',
    }
}

Production = Redis 권장. LocMemCache 는 프로세스 별 개별 → 여러 worker 에서 부정확.

대안 (외부 도구)

flowchart TD
    Q{Rate limit 위치}
    Q -->|"App 단위"| DRF["DRF Throttle"]
    Q -->|"Nginx / Traefik"| RP["Reverse Proxy"]
    Q -->|"API Gateway"| AG["Kong / AWS API Gateway"]
    Q -->|"CDN"| CDN["Cloudflare Rate Limit"]

여러 계층 조합 권장. DRF = 앱 로직 기반, CDN = 광범위 DDoS 방어.

자세한 rate limit 알고리즘은 rate-limiting.

다른 프레임워크

FrameworkPaginationThrottling
DRF3종Class based
FastAPI없음slowapi
SpringPageablebucket4j
Express없음express-rate-limit
Railskaminari, will_paginaterack-attack

흔한 함정

WARNING

  1. PAGE_SIZE 미설정 + PageNumberPagination = 사용 시 에러.
  2. Cursor pagination 정렬 안 정함 = 에러.
  3. 큰 offset (page=1000) = DB 부담. Cursor 로 마이그레이션.
  4. Throttle LocMemCache = worker 마다 별개. Redis.

관련 위키

이 글의 용어 (5개)
[Concurrency] Rate Limiting: token bucket, sliding windowconcurrency
정의 Rate Limiting = 시간당 요청 수 제한. API 보호, fair use, 비용 제어, DDoS 완화. 5가지 알고리즘 1. Fixed Window - 단순. - …
[Concurrency] Retry + Exponential Backoff + Jitterconcurrency
정의 Retry with Exponential Backoff + Jitter = 실패 시 점점 긴 간격으로 재시도, 랜덤 분산. 분산 시스템의 thundering herd / r…
[DRF] Filtering: django-filter, SearchFilter, OrderingFilterdjango
정의 Filtering = 쿼리 파라미터로 결과 제한. 3가지 backend + custom. 3가지 내장 Backend 설치 + 설정 1. DjangoFilterBackend …
[DRF] Views: APIView, GenericViews, Mixins, FBVdjango
정의 DRF View 4가지 스타일. 자유도 vs 자동화 트레이드오프. 4가지 스타일 | 스타일 | 코드량 | 자유도 | |---|---|---| | | 유사 Django FBV…
[Redis] Cache Patterns: Cache-Aside, Stampede, Evictiondatabase-internals
정의 Cache Pattern 은 Redis (또는 다른 캐시) 를 어떤 경로로 읽고 쓰는지 의 디자인 결정. 같은 인프라로도 패턴 선택에 따라 일관성 / 지연 / 부하 가 완전…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기