본문 바로가기
Web

[Paging] 페이지네이션을 알고 사용하자

by 수짱수짱 2023. 10. 2.

해당 게시글은 "프로그래머스 데브코스 4기"의 팀 내 프로젝트 기록용으로 TECH BLOG에 직접 작성한 글입니다.


페이징(Paging, Pagination)이란?

프로젝트를 진행하면서 “베스트 상품 페이징”처리가 필요하게 되었고 페이징 처리를 해본적 없는 나는..! 페이징에 대해 먼저 알아보는 것이 먼저였다!

 

먼저, 페이징이 필요한 이유는 십만개의 데이터가 존재할 때 이 데이터를 한 화면에 한 번에 보여줄 수 없기 때문에 일정 기준으로 끊어서 데이터를 표시하기 위해 필요하다.

만약 서버에 한 요청이 십만개의 데이터를 요청하는 것이고, 이와 같은 요청이 여러 사용자들에 의해 발생한다면? 서버에선 OOM이 발생하는 문제가 생길 수도 있다.

이와 같은 문제들을 해결하기 위해 페이징이 필요하다.

 

우리가 가장 자주 접할 수 있는 이전, 다음 페이지 기능이라던가

아니면 인스타그램과 페이스북 하단 스크롤이 끝나지 않고 무한대로 내려가는 기능이라던가

이러한 기능들이 전부 페이징 기술을 이용해서 구현된 예시이다

 

페이징은 백엔드 뿐만 아니라 프론트에서도 구현이 되어야 사용할 수 있다

필요한만큼만 백엔드에 요청하고 응답받아 해당 데이터를 출력하는 방식이다

(단, 꼭 백엔드와 프론트가 같은 페이징 기법을 사용할 필요는 없음을 유의하자)

 

페이징을 사용해서 좋은 점이 뭔가요?? 라고 할 수 있다.

십만개의 데이터를 한 번에 받아온다면 데이터 로딩 속도도 굉장히 오래 걸릴 것 이고 동시에 수많은 접속자가 데이터를 요청한다면 서비스 과부화가 걸릴 수도 있다.

이러한 부하와 데이터 로딩 속도를 효율적으로 조절해주는 것이 페이징이다!

 

페이징에 사용되는 인자로는 limit, offset이 있다
(프론트에서 limit, offset 인자를 이용해 백엔드에게 요청)

limit보여줄 콘텐츠의 수를 의미하고 offset현재 위치를 의미한다 ⇒ offset 방식

 

🧫 페이징 종류

가장 크게 2가지로 Offset 방식과 Cursor 방식의 페이징 기법이 존재한다.

 

1️⃣ Offset 방식

offset, limit 를 통하여 select의 전체 결과 중 일부만 가져오는 방식

spring 프레임워크의 pagenable 인터페이스는 offset방식으로 이루어져 있다(limit, offset 인자)

Offset 방식의 게시판 페이징

 

보통 위 사진과 같은 게시판 형식에 많이 적용된다 ⇒ 보통 새로운 게시글을 보기 위해선 새로고침 기능을 사용!

SELECT * FROM product LIMIT {페이지 당 출력할 자료 갯수} OFFSET {오프셋}
SELECT * FROM product LIMIT 40 OFFESET 0; # 1~40 출력
SELECT * FROM product LIMIT 40 OFFESET 40; # 41~80 출력
SELECT * FROM product LIMIT 40 OFFESET 80; # 41~80 출력

단순해서 그저 적용하면 좋을 것 같지만 해당 방식을 사용할 때 주의해야 할 점이 있다.

offset 방식은 앞 부분의 데이터(0부터 40)를 조회할 땐 문제가 되지 않는다. 하지만 뒷 부분의 데이터라면?

1,000,000부터 40개를 조회할 경우 굉장히 느린 속도가 나올 것이다.

즉, offset의 수가 늘어날 수록 느려진다.

 

왜 뒷 부분 데이터를 조회할 수록 느려질까?

데이터의 갯수는 변경될 수 있으므로 매번 데이터를 확인하기 위해 해당 offset만큼 데이터를 쭉 나열하기 때문이다.

그럼 1억번째 부터 40개라면? 1억번째 까지 데이터를 나열해야 하므로 속도가 굉장히 느릴 것이다..

offset값이 높을수록 느려지는 실행 속도

 

 

그리고 offset의 주된 특징중 하나는 정적데이터에 적합하다는 것이다

사용자가 화면을 보는동안 insert, delete가 자주 일어나게 된다면 데이터 중복/누락 문제가 발생할 것이다

아래에서 데이터중복에 대한 예를 들어보자!

  1. 사용자가 1페이지를 요청함
  2. 한 편 판매자가 등산화와 테니스 채 상품을 추가(insert), 사용자는 기존 데이터(파란색)를 보고 있음
  3. 사용자가 2페이지를 요청함, 1페이지에서 이미 보았던 러닝 힙색과 골프화를 중복하여 보게 된다.
  4. 데이터가 추가되지 않았을 경우의 결과

해당 문제가 발생하는 이유는 offset은 cursor와 다르게 “순번”이 기준이기 때문이다

즉, 어딜보고 있었는지 기억하지 못하고 오로지 “규칙”에 의해서 조회를 하는 것이다 ⇒ offset만큼 건너뛸뿐!

 

동시에 기준이 “순번”이기 때문에 해당 순번을 세아리기 위해 뒷 부분 데이터를 조회할 수록 느려지는 것이다.

뒤에서 설명할 cursor와 비교하면 cursor는 순번이 아닌 지칭 대상이 있으므로 이러한 문제가 발생하지 않는다

 

❗offset 정리

장점

  • 구현이 단순하다 (쿼리가 단순하다)
    • spring 프레임워크의 pageable은 offset방식이다
  • 다양한 정렬 방식을 쉽게 구현할 수 있다
  • 자주 바뀌지 않으(정적인 데이터)면서 데이터의 양이 적다면 고려해볼만 하다.

단점

  • 데이터 중복이 발생할 수 있다
  • 페이지 뒤로 갈수록 쿼리 실행 시간이 오래걸린다
  • 데이터의 잦은 추가와 삭제가 이루어질 때 데이터 누락과 중복이 발생할 수 있다
    • 이러한 단점은 실시간으로 빠르게 데이터가 삭제되고 추가되는 SNS에는 속도도 느리고 치명적인 오류가 발생할 수 있다

 

2️⃣ Cursor 방식 (=seek Method, Keyset Pagination)

Cursor 방식의 페이징

 

cursor는 어떠한 레코드를 가리키는 포인터를 의미한다

이 cursor가 가리키는 레코드로부터 일정 갯수만큼 가져오는 방식이 Cursor 페이징 방식이다

커서 방식은 페이스북, 인스타의 게시글 목록에서 많이 사용된다

# 가장 처음 쿼리
SELECT * FROM product LIMIT 40; # 1~40

# 마지막 id값을 통해 다음 페이지 쿼리
# 기준값 보다 큰 id를 가진 자료를 40개 보여줘
SELECT * FROM product WHERE id > {기준값} LIMIT 40;
SELECT * FROM product WHERE id > 40 LIMIT 40; # 41~80

# ORDER BY절을 통해 id 내림차순으로 가져올 수 있다
# 그럼 다음 id값은 작아져야 하므로 비교 연산자를 < 를 사용한다
SELECT * FROM product ORDER BY id DESC LIMIT 40; # 200~161
SELECT * FROM product WHERE id < 161 ORDER BY id DESC LIMIT 40; # 160~121
SELECT * FROM product WHERE id < 121 ORDER BY id DESC LIMIT 40; # 120~81

 

cursor방식은 데이터 누락, 중복의 문제가 없을 것 같지만 데이터 정렬의 쿼리문 작성에 있어서 유의해야 한다!

예를 들어보자. 아래는 price가 25,000 미만인 제품으로 정렬을 요청하는 쿼리문이다.

SELECT id, price FROM product ORDER BY price DESC LIMIT 5; # 1.처음 요청
SELECT id, price FROM product WHERE price < 25000 ORDER BY price DESC LIMIT 5; # 3.잘못된 요청

price중복 가능성이 있는 값이다. (다른 상품도 25,000원 일 수 있다)

따라서 price를 기준으로 정렬한 결과로는 중복된 데이터를 생략하게 된 문제가 발생했다.

따라서, 중복 가능성이 있는 값이 아닌 고유한 id값을 함께 정렬 기준으로 세운다면 중복되는 데이터를 누락하지 않을 것이다.

SELECT id, price FROM product ORDER BY price DESC, id DESC LIMIT 5; # 1. 처음 요청

# 3. 올바른 요청
SELECT id, price FROM product 
WHERE price < 25000 
OR (price = 25000 AND id < 82) # 25000원이면서 가장 최근에본 82번보다 작은 id값을 가져와
ORDER BY price, id DESC LIMIT 5; # id도 함께 정렬함

같은 price에 대해 id값이 내림차순으로 정렬된 모습도 확인 가능

price 뿐만 아니라 id라는 고유한 값을 통해 price값이 중복되는 상황을 예외 처리함으로서 원하는 결과를 가져올 수 있게 되었다.

 

🔥 가장 중요한 것은 무조건 id가 중복되지 않을 거라는 믿음을 가져서는 안된다..!!!

❗cursor 정리

장점

  • offset방식과 비교하면 속도가 굉장히 빠르다

  • 정렬이 필요없고 대용량 데이터의 추가와 삭제가 자주 일어나는 도메인에 적합하다 (ex. SNS)

단점

  • 구현이 어려울 수 있다
  • 제한된 정렬 조건
    • 정렬이 복잡해지면 구현이 더 까다로워진다는 단점이 있다

결론

예로, 읽어올 데이터가 1억개라면 효율이 떨어지므로 커서 페이징을 적용하는 것이 좋겠다

반대로 복잡한 정렬이 필요하다면 cursor를 이용할 때 데이터 누락의 문제가 발생할 수 있기도 하다.

따라서 꼭 offset보다 cursor 방식이 좋다기 보다는 상황에 따라 적절하게 적용하는 것이 중요하다.


Reference (꼭 읽어보세용!)