📌 이 포스팅에서는 Offset 기반 Pagination과 Cursor 기반 Pagination에 대해 정리하였습니다.
🔥 Pagination 이란?
🔥 Offset 기반 Pagination
🔥 Cursor 기반 Pagination
🔥 슬라이싱 사용 시, 주의할 점
✔️ Pagination이라 한정된 네트워크 자원을 효율적으로 사용하기 위해 데이터를 분할하여 DB에서 가져오는 방법을 의미하다.
✔️ 예를 들어, 게시글 list를 요청할 때, 서버에서 모든 게시글을 데이터로 전달하면, 네트워크 통신에 있어 큰 비용이 발생한다.
✔️ 이에 처음에는 20개의 게시글을 보내주고 사용자가 스크롤을 내리거나, 더보기 버튼을 누르는 경우, 이어서 다음 데이터부터 20개를 추가로 보내줌으로써 데이터를 분할하여 전달하는 방식에 쓰인다.
✔️ 또는 1페이지, 2페이지 버튼을 누르는 것도 Pagination의 일종이다.
✔️ 즉, Pagination은 네트워크의 비용을 감소시키고 빠른 응답을 위해 정렬된 DB의 리소스를 분할하여 전달하기 위한 방법이다.
✔️ Pagination은 전통적으로 사용한 Offset 기반과 실무에서 사용하는 Cursor 기반으로 나뉜다.
✔️ 오프셋 기반 페이지네이션(Offset-based Pagination)
✔️ 커서 기반 페이지네이션(Cursor-based Pagination)
✔️ offset-base-pagination은 전통적으로 사용된 pagination 방법이다. OFFSET 값을 포함한 SQL 쿼리문을 이용한다.
✔️ 즉, OFFSET은 row를 건너띌 시작점을 의미하고 LIMIT을 통해 row의 수를 제한하여 DB에 요청하는 방식이다.
select * from post limit 10 offset 0 # 👈 0번부터 9번까지 10개의 row select * from post limit 10 offset 10 # 👈 10번부터 9번까지 10개의 row select * from post limit 10 offset 20 # 👈 20번부터 9번까지 10개의 row select * from post limit 10 offset 20 # 👈 30번부터 9번까지 10개의 row
✔️ offset-base-pagination는 오랫동안 사용되왔고, 네트워크 자원을 효율적으로 사용하기 위해 가장 대중적인 방식이다.
✔️ 단, 0번 row부터 9번 row까지 요청한 뒤, 그 다음페이지를 요청하기 전 데이터가 데이터가 생성되었고 추가된 데이터가 지정한 정렬방식에 의해 0번~9번 사이에 위치해있다면, 해당 데이터를 건너뛰어버리는 문제가 발생한다.
✔️ 해당 데이터를 볼 수 없다는 의미는 이미 본 데이터를 중복으로 보게된다는 가능성을 포함한다.
✔️ 반대로 다음 row들를 읽어드리기 전에 데이터가 삭제된다면 뒤에 있던 row가 앞으로 당겨지기 때문에 특정한 데이터가 누락된다.
✔️ 이에 데이터가 잦은 수정이 발생되는 경우, offset-base-pagination는 데이터 중복 또는 누락 issue가 발생한다는 문제가 존재한다.
✔️ 뿐만아니라 offset-base-pagination은 성능 issue가 존재한다. 극단적으로 1억번째 페이지에 있는 값을 찾고 싶다면, OFFSET에 매우 큰 숫자가 들어간다.
✔️ offset-base-pagination은 정렬기준(order by)에 대해 지정된 OFFSET까지 모두 만들어 놓은 후 LIMIT에 지정한 갯수로 자르는 방식이기 때문에 데이터가 많아질 수록 이에 비례하여 속도가 느려질 수 밖에 없다.
✔️ offset-base-pagination은 우리가 원하는 데이터가 ‘몇 번째’에 있다는 데에 집중하고 있다면, cursor-base-pagination은 우리가 원하는 데이터가 '어떤 데이터의 다음'에 있다는데에 집중한다.
✔️ 즉, "n개의 row를 skip 한 다음 10개 주세요."가 아니라, "이 row 다음꺼부터 10개 주세요."를 요청하는 방식이다.
✔️ 이에 cursor-base-pagination는 WHERE절이 필요하고, 정렬 기준에 따라 중복된 데이터가 존재할 가능성이 있다면 OR절이 추가로 필요하다.
SELECT * FROM post WHERE id <= (Id Cursor : 996) ORDER BY id DESC LIMIT 10
✔️ OR절이 필요한 이유는 create_at을 통해 생성된 날짜로 정렬 했을 때, 동시에 생성된 데이터가 여럿 존재한다면 그 시간에 생성된 1개의 row를 제외하고 모두 무시될 수 있기 때문이다.
✔️ Django ORM으로 Slicing을 사용하는 것은 SQL문으로 OFFSET, LIMIT을 사용것과 같다.
✔️ 아래 ORM에서 슬라이싱을 사용하면, Post 테이블에서 1번,2번 row를 가져온다.
Post.objects.all()[1:3]
✔️ SQL문으로는 아래와 같다. 즉, OFFSET은 시작점이되는 것이고, LIMIT은 OFFSET으로부터의 가져올 row의 수이다.
mysql>>> SELECT * FROM post LIMIT 2 OFFSET 1
✔️ 아래 처럼 음수로 슬라이싱을 하면, 슬라이싱 개념에서는 논리적으로 문제가 없지만 ORM을 사용할 때에는 에러가 발생한다. 이에 음수를 사용하는 것 보다는 역순으로 정렬 후 사용해야 한다.
Post.objects.all()[:-1] # ValueError: Negative indexing is not supported.
✔️ 슬라이싱 개념에서 step 사용할 경우, Query에 대응되지 않는다.
✔️ 아래는 예상한데로 일반적인 query를 반환한다.
Post.objects.all()[0:3] # <QuerySet [<Post: 세번째 메세지>, <Post: 두번째 메세지>, <Post: 첫번째 메시지>]>
✔️ 슬라이싱에서 step 1은 생략한 것과 같지만, query가 아닌 list로 반환하는 것을 볼 수 있다.
Post.objects.all()[0:3:1] # [<Post: 세번째 메세지>, <Post: 두번째 메세지>, <Post: 첫번째 메시지>]
✔️ 모든 ORM은 Lazy Loading의 특성을 지니고 있다. 실제 query가 필요한 시점에 DB에 요청하는 게으른 녀석들인데, setp을 사용할 경우 즉시 DB에 Query를 보내고 가져온 Queryset을 list로 변환한다.
✔️ 즉, 지금 당장 query를 DB로 보내 반환받아야할 때가 아니라면, 불필요한 query를 발생시키기 때문에 step은 사용하지 않는 것이 좋다.