본문 바로가기
프로그래밍/Django

Django 대량의 list 요청시 Cursor-based Pagination

by 우주를놀라게하자 2025. 2. 18.
반응형
SMALL

Django를 사용하다보면 기본적으로 사용하는 DRF Pagination을 사용해왔다. 그러나 대량의 데이터를 페이지네이션을 하게되면, 퍼포먼스 및 최적화 관점에서 좋지 않다.

Pagination의 종류


구현 방법

  • 오프셋 기반 페이지네이션(Offset-based Pagination)
  • 커서 기반 페이지네이션(Cursor-based Pagination)

문제 상황


기존의 오프셋 기반 페이지네이션과 커서 기반의 페이지네이션의 차이는 무엇이고 왜 대용량의 페이지를 처리할때는 오프셋 기반보다 커서 기반으로 처리하는것이 좋은걸까?

기본적으로 많이 사용하는 Pagination은 Offset 기반의 Pagination이다.

Offset을 MySQL에서라면 간단하게 LIMIT 쿼리에 콤마를 붙여 ‘건너 뛸’ row 숫자를 지정하면 된다.

Offset-based Pagination의 간략한 설명과 문제점

SELECT * FROM `product`
LIMIT 10 OFFSET 10;
  • OFFSET: 뛰어넘는 Row 개수
  • LIMIT: 가져오는 개수 제한
  • 왜? Why? 데이터 개수는 변경될 수 있기 때문에 매번 데이터를 확인하는(Full-scan) 해당 offset 수 만큼 지나가야하기 때문이다.
  • 문제는 OFFSET에서 발생하게 된다. 예를들어 앞부분의 데이터를 조회하는 경우는 문제가 되지 않지만, 10만부터 40개씩 등과 같은 방식으로 조회할 경우 퍼포먼스가 굉장히 느려진다.
    • 아래 코드를 보게 되면 이해가 빠를것이다. 기본적으로 DB는
      • **ORDER BY id ASC**에 따라 전체 데이터셋을 정렬하게 되고,
      • OFFSET 100만개의 행을 먼저 스캔하게 된다.
      • 그 다음 10개의 결과를 반환한다.
SELECT * FROM `product`
ORDER BY id ASC
LIMIT 10 OFFSET 1000000;

Cursor-based Pagination


SELECT * FROM `product`
WHERE id > {이전 페이지의 마지막 id}
LIMIT 10;
  • Offset을 사용하지 않아서 zero Offset 방식이라고도 한다.
  • 위의 방식으로 처리하면 DB의 인덱스를 활용하여 해당위치에서 바로 검색이 가능하다.
    • 아래의 형태로 검색한다고 생각하면 될 것 같다!
     

 

 

사용법


  1. Settings에 선언하여 전역으로 사용하는 방법
# Settings.py

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
    'PAGE_SIZE': 10,
}
  1. rest_framework의 CursorPagination을 상속받아서 처리
class CommonCursorPagination(CursorPagination):
    page_size_query_param = 'page_size' # 유저가 동적으로 page_size를 받도록 허용
    page_size = 10 # 몇개를 받을 것인가?
    ordering = '-created'
    cursor_query_param = "cursor"  # cursor_query_param 옵션을 따로 사용하지 않으면, 기본으로 cursor 옵션이 들어간다. 
 

 

views.py
# 전역으로 사용하지 않고 따로 상속을 받은 후 View에서 주입하여 처리하는 방식
class MyViewSet(
    viewsets.GenericViewSet,
    mixins.CreateModelMixin,
    mixins.ListModelMixin,
    mixins.DestroyModelMixin,
    mixins.UpdateModelMixin,
    mixins.RetrieveModelMixin,
):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = CommonCursorPagination

결과


 "next": "<http://localhost:8000/link/me/?cursor=cD0yMDI1LTAyLTA4KzAwJTNBMDIlM0ExNy43ODUxMzklMkIwMCUzQTAw>",
  "previous": null,
  "results": [
    {
      "id": "fc98b586-b829-4bd1-b5a7-e841cf73c1bf",
반응형
LIST