Back-end

[Back-end] 페이지네이션의 구현방식 #cursored-based-pagenation #offset-based-pagination

helloyukyung 2023. 12. 7. 13:53

페이지네이션

웹에서 리스트 페이지를 구현할 때 빠지지 않는 게 페이지네이션이다.
서버에서 데이터를 가져올 때 모든 데이터를 한 번에 가져올 수는 없다.
대규모 데이터를 한 번에 가져오게 되면, 서버와 클라이언트 리소스에 부담이 가게 된다.
또한, 수백 수천 개의 행을 한 번에 표시해서 보여주는 것은 좋은 사용자 경험이 아니다.
따라서 정해진 개수만큼 나눠 데이터를 가져오는 것이 필요하다.

offset-based-pagination vs cursor-based-pagination

서버 측에서 페이지네이션을 구현할 때, 구현방식에는 2가지 유형이 있다.

  • offset-based-pagination: DB의 offset쿼리를 사용하여 ‘페이지’ 단위로 구분하여 요청/응답
  • cursor-based-pagination: cursor개념 사용, 유저에게 응답해 준 마지막 데이터 기준으로 다음 n개 요청/응답

오프셋 기반 페이지네이션(offset-based-pagination)

먼저, 오프셋 기반 페이지네이션에 대해 알아보자.

{
  page_size: 50,
  page: 0
}

오프셋 기반 페이지네이션은 비교적 구현이 간단하고, 가장 일반적으로 사용되는 방법이다.
클라이언트에서 보내는 GET /products의 params는 위와 같아진다.

하지만 이 방식에는 2가지 문제점이 존재한다.

1. 중복 데이터 노출 가능성

예를 들어 0 페이지에서 20개의 상품들을 불러와 첫 번째 페이지에 띄워주었다고 해보자.
그런데, 고객이 첫 번째 페이지의 상품들을 보고 있는 사이, 운영팀에서 상품 N개를 업데이트했다고 하면 어떻게 될까?
유저가 첫 번째 페이지를 둘러보고 이후 두 번째 페이지를 눌렀을 때,

index가 하나씩 밀려져, 마지막 N개의 상품이 중복 노출된다.(등록일 기준 내림차순의 경우)
반대로, N개의 상품을 삭제했다면, 2번째 페이지로 넘어갔을 때, 고객은 N개의 상품을 못 보게 된다.😰

 

2. OFFSET 쿼리의 퍼포먼스 이슈

대부분의 낮은 데이터양의 경우 offset 쿼리는 느리지 않지만, 높은 offset값을 가질 경우 퍼포먼스의 문제가 생긴다.

 

예를 들어, query에서 "LIMIT 50000, 20"와 같은 절을 사용할 경우,
실제로는 50,020개 행을 통과하고 처음 50,000개를 버리도록 요청한다.
따라서 DB의 데이터가 양이 많아질수록 퍼포먼스는 떨어지게 된다.

 

누가 50000페이지로 건너뛰려고 하겠어?라고 웃어넘길 수 있지만, 가능한 예시가 존재한다.😱

  • 검색 엔진(Google / Bing / Yahoo / DuckDuckGo 등)이 당신의 웹사이트를 색인화하려고 한다. 해당 웹사이트에는 약 100,000개의 페이지가 있다. 검색 봇이 뒤에 있는 50,000페이지를 인덱싱하여 색인화하려고 할 때 애플리케이션은 어떻게 반응할까?
  • 대부분의 웹 애플리케이션에서는 다음 페이지뿐만 아니라 마지막 페이지로 이동할 수 있도록 허용한다. 사용자가 2페이지를 방문한 후 50,000 페이지로 건너뛰려고 하면 어떻게 될까?
  • 사용자가 Google 검색 결과에서 20,000 페이지로 이동하여 거기서 좋아하는 콘텐츠를 찾아 Facebook에 올려 1000명의 친구에게 읽기를 권하는 경우 어떻게 될까?

 

커서 기반 페이지네이션(cursor-based-pagination)

오프셋 기반 페이지네이션은 우리가 원하는 데이터가 '몇 번째'에 있다는데 집중하고 있다면

커서 기반 페이지네이션은 우리가 원하는 데이터가 '어떤 데이터의 다음'에 있다는 데에 집중한다.

즉, n개의 row를 skip 한 다음 10개 주세요가 아닌 이 row 다음 거부터 10개 주세요로 요청한다.

{
    page_size: 50,
    cursor: `${고유ID}`
}

클라이언트에서 보내는 GET /products의 params는 위와 같아진다.

커서 기반 페이지네이션 방식은 우리의 데이터가 몇 번째에 존재하는지 처음부터 검사하지 않는다.

해당하는 cursor(id)의 row를 찾고 그다음부터 page_size 만큼 보내줄 수 있다.

 

커서 기반 페이지네이션은 위 offset-based가 가지고 있었던 문제점을 모두 해결해 줄 수 있다.

실시간으로 업데이트되는 데이터들을 누락 없이 보여줄 수 있으며,
일부 데이터에서 찾는 것이 아닌, unique한 데이터를 찾고,
그 이후 데이터를 보여주기 때문에 성능적인 부분에서도 이점을 가지고 있다.

하지만 커서 기반 페이지네이션을 구현하는데도 주의사항이 필요하다.

1. unique한 cursor의 필요성

cursor값이 unique해야한다.

만약 동일한 id가 존재한다면, 특정 구간에서 무한루프를 일으킬 수 있다.

2. 구현 오버헤드

커서 기반 페이지네이션을 구현하는 것은 때로 어려울 수 있고, 특정 필드에 대한 정렬 수행이 제한될 수도 있다.

3. Pagination UI

출처 : https://medium.com/@mathroda/infinite-scroll-with-paginated-api-calls-in-jetpack-compose-4c3facfb50ed

커서 기반 페이지네이션을 웹에서 구현하고자 한다면, 페이지네이션 바가 있는 Pagination은 구현하지 못한다.
(ex. 유저가 1페이지에서 제품을 보고 있다고 할 때, 바로 5페이지를 보여주기 위한 cursor 값을 모르기 때문)

따라서 커서 기반 페이지네이션을 구현하게 된다면, Infinity Scroll이나, Load more 같은 UI로 페이지네이션 구현을 해야 할 것이다.

결론

정리하자면,

  1. 변화가 거의 없어 중복 데이터가 도출될 걱정이 없는 경우
  2. 검색엔진이 인덱싱을 할 이유도, 유저가 마지막 페이지를 갈 이유가 없는 경우
  3. 데이터 양이 적어 퍼포먼스의 걱정이 적은 경우
  4. 페이지네이션 바를 구현해야 하는 경우

이러한 경우가 아니라면, 커서 기반 페이지네이션 방식으로 페이지네이션을 구현하는 것이 바람직할 것이다.

 

 

알고 구현하는 것과 모르고 구현하는 것에 대한 차이는 매우 크다고 생각한다.

백엔드 구현지식이지만, 프론트도 알아야 하는 개념이라고 생각되어 정리하게 되었다.

 

참고 :
https://www.eversql.com/faster-pagination-in-mysql-why-order-by-with-limit-and-offset-is-slow/

https://medium.com/@mathroda/infinite-scroll-with-paginated-api-calls-in-jetpack-compose-4c3facfb50ed

https://velog.io/@minsangk/%EC%BB%A4%EC%84%9C-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-Cursor-based-Pagination-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0