Pagination
개요
우리가 보는 게시판서비스는 하단에 1, 2, 3…과 같이 숫자가 나오고 첫 번째 페이지를 읽고 다음 페이지를 누르면 첫번째 페이지에서 읽은 페이지 그 다음 페이지가 나온다. 이를 페이지네이션이라고 한다.
만약 페이지네이션이 적용되지 않아서 한 화면에 끝도 없이 정보가 나온다고 해보자. 마치 애니메이션에 나오는 양피지를 읽듯이 스크롤을 계속 내려야할 것이고 어느 곳까지 읽었는지 알기도 힘들 것이다.
단순 사용자의 편의 말고도 성능상의 이유도 있다.
데이터베이스에서 데이터를 불러올 때 모든 데이터를 다 가져와서 처리해야한다면 성능 저하가 나타날 수 있다. 따라서 리스트를 처리할 땐 이와 같은 전략을 사용한다.
페이지네이션은 크게 두 가지 방법이 존재하는데 한 번 알아보자.
오프셋 기반 페이지네이션
페이징을 위한 데이터베이스 문법은 limit, offset으로 다음과 같다.
SELECT * FROM post LIMIT 10 OFFSET 0;
이렇게 하는 방식이 우리가 게시판 서비스에서 보는 1, 2, 3…과 같이 나오는 페이징 방식이다.
테이블의 일부를 가져오는 문법이기 때문에 게시글을 읽을 때 내가 글을 읽는 사이 다른 사용자가 새롭게 글을 작성한다면 다른 페이지를 요청하더라도 이전에 봤던 글이 나올 수 있다는 특징이 있다.
커서기반 페이지네이션
위의 쿼리의 실행 계획을 살펴보자.
EXPLAIN ANALYZE SELECT * FROM post limit 10 OFFSET 0;
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Limit: 10 row(s) (cost=6.65 rows=10) (actual time=0.137..0.143 rows=10 loops=1)
-> Table scan on display_info (cost=6.65 rows=59) (actual time=0.135..0.14 rows=10 loops=1)
|
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
그리고 그 다음 페이지
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Limit/Offset: 10/10 row(s) (cost=6.65 rows=10) (actual time=0.0875..0.0942 rows=10 loops=1)
-> Table scan on display_info (cost=6.65 rows=59) (actual time=0.0772..0.0897 rows=20 loops=1)
|
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
첫 번째 페이지 조회시 읽은 row는 10건, 두 번째 페이지 조회시 읽은 row는 20건이 된다.
만약 LIMIT a OFFSET b라고 한다면 데이터베이스는 a + b만큼의 데이터를 테이블에서 읽고 불필요한 부분은 버리는 식으로 동작한다.
따라서 Offset기반 페이지네이션은 페이지가 뒤로 갈수록 읽어야하는 데이터의 총량이 많아져 저하된 성능을 보일 수 있다는 단점이 있다.
이런 단점을 보완할 수 있는 방법이 바로 커서 기반 페이지네이션 방법이다.
SELECT * FROM post where id > {:cursor} LIMIT 10;
쿼리 실행 계획을 보면…
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Limit: 10 row(s) (cost=10.1 rows=10) (actual time=0.0296..1.77 rows=10 loops=1)
-> Filter: (display_info.id > 10) (cost=10.1 rows=49) (actual time=0.0284..1.76 rows=10 loops=1)
-> Index range scan on display_info using PRIMARY over (10 < id) (cost=10.1 rows=49) (actual time=0.0266..1.76 rows=10 loops=1)
|
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Limit: 10 row(s) (cost=8.08 rows=10) (actual time=0.0302..0.0618 rows=10 loops=1)
-> Filter: (display_info.id > 20) (cost=8.08 rows=39) (actual time=0.0291..0.0586 rows=10 loops=1)
-> Index range scan on display_info using PRIMARY over (20 < id) (cost=8.08 rows=39) (actual time=0.0272..0.0544 rows=10 loops=1)
|
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
이렇게 딱 필요한 만큼만 읽는다.
만약 DB의 중복되지 않는 값을 기준으로 커서 기반 페이징을 적용한다면 위에서 봤던 오프셋 페이징의 단점을 보완할 수 있다.
DB의 PK는 중복이 불가능하고 누군가 글을 작성하더라도 마지막으로 응답이 나간 id값의 이후 혹은 이전 데이터를 불러오기 때문에 중복이 생기지 않는다.
커서기반 페이지네이션은 주로 무한 스크롤 방식으로 사용된다.
모바일 환경의 경우 작은 요소를 클릭하기 힘들다. 반면 손가락으로 스크롤하기는 쉽기 때문에 주로 모바일 환경에서 많이 사용된다.
PC환경에서도 검색이나 서핑같은 경우 집중해서 살펴보지 않고 계속해서 새로운 정보를 보는 경우가 많은데 이런 경우도 무한 스크롤을 적용되어있는 경우가 많다. 유튜브의 경우 PC환경에서 접속하더라도 무한 스크롤이 적용되어있다.
그럼 무조건 커서기반 페이지네이션이 좋은걸까?
그렇진 않다. 예를 들어 페이지의 중간중간을 건너 뛰어야한다면 어떨까?
게시판에서 처음 페이지에서 맨 끝 페이지로 이동하는 것과 같이 불연속적인 요청이 들어올 경우 사용하지 못한다. 커서기반 페이지네이션은 마지막으로 유저에게 응답이 나간 커서 값의 이후 값을 다음 데이터의 시작 값으로 갖기 때문이다.
이런 경우는 오프셋 페이지네이션을 사용해야한다.