1. Pagination이란
Pagination이란 검색 결과를 가져올 때 데이터를 쪼개 번호를 매겨 일부만 가져오는 기법이다
1.1 Pagination을 사용하는 이유
사용자가 애플리케이션을 사용 중 게시판, 상품 목록 등을 요청할 때 결과 값이 총 100만개일 경우 매번 전체를 전부 가져오게 되면 매우 느려지며 사용자는 불편을 느끼고 애플리케이션을 이탈할 것이다. 하지만 데이터를 조금씩(20개~100개) 나눠서 가져오고 사용자가 원하는 경우 다음 데이터를 가져오게 되면 훨씬 빠르고 사용자도 애플리케이션에 대해 만족할 것이다. 이러한 이유로 사용하는 것이 Pagination이다.
1.2 Pagination 구현 방법의 종류
- Offset 방식 : Offset과 limit 예약어를 통하여 select의 전체 결과 중 일부만 가져오는 방법이다.
- Cursor 방식 : cursor는 어떠한 레코드를 가리키는 포인터이고 이 cursor가 가리키는 레코드부터 일정 개수만큼 가져오는 방식이다. Seek Method, Keyset Pagination이라고도 한다.
2. Offset 방식의 Pagination 처리
클라이언트는 페이지 당 요청하는 자료의 개수와 현재 페이지 번호를 파라미터로 요청한다.
서버에서 오프셋 값을 구한다 이 값은 쿼리의 OFFSET 부분에 입력되는 값이다.
오프셋 = (현재 페이지 번호 - 1) * 페이지 당 요청하는 자료 개수
현재 페이지 | 페이지 당 요청 개수 | 수식 | 오프셋 |
---|---|---|---|
1 | 40 | (1 - 1) * 40 | 0 |
2 | 40 | (2 - 1) * 40 | 40 |
3 | 40 | (3 - 1) * 40 | 80 |
4 | 40 | (4 - 1) * 40 | 120 |
5 | 40 | (5 - 1) * 40 | 160 |
서버는 데이터베이스와 통신하여 쿼리에 LIMIT와 OFFSET을 통하여 자료를 요청한다.
SELECT * FROM product LIMIT {페이지 당 자료의 개수} OFFSET {오프셋}
SELECT * FROM product LIMIT 40 OFFSET 0; 1-40 출력
SELECT * FROM product LIMIT 40 OFFSET 40; 41-80 출력
SELECT * FROM product LIMIT 40 OFFSET 80; 81-120 출력
SELECT * FROM product LIMIT 40 OFFSET 120; 121-160 출력
SELECT * FROM product LIMIT 40 OFFSET 160; 161-200 출력
3. Offset 방식의 단점
3.1 뒷부분 쿼리 시 속도 저하
Offset 방식은 0부터 40을 조회하거나 100부터 40개를 조회할 때처럼 앞부분의 데이터를 조회할 경우 문제가 되지 않지만 1,000,000부터 40개를 조회할 경우 등 Offset의 수가 늘어날수록 굉장히 느려진다. 그 이유는 데이터의 개수는 변경될 수 있기 때문에 매번 데이터를 확인하여 해당 Offset 수 만큼 지나간 후 데이터를 반환한다.
3.1.1 OFFSET 값에 따른 쿼리 속도 차이 테스트
사용 데이터베이스 : Mysql 8.0
100,000개의 자료를 가지고 있는 product 테이블에 값을 40개 가져오는 쿼리가 있다. 이 쿼리를 OFFSET 0, 10,000, 100,000, 250,000, 500,000, 750,000, 1,000,000의 오프셋 값의 차이를 두고 실행한다.
쿼리마다 테스트 전 MYSQL 서버를 재시작하였다. 그 후 쿼리를 11번 시행한다. 첫번째 실행은 InnoDB 버퍼풀이 초기화 되었기 때문에 속도가 느리고 실서버에서는 거의 없는 경우이기 때문에 이것을 생략한 나머지 10개의 데이터의 평균을 낸다.
MYSQL 8.0은 쿼리를 캐시하지 않기 때문에 SQL_NO_CACHE는 추가하지 않았다.
테스트 쿼리
SELECT ...생략
FROM marketplace.product p
LEFT JOIN category c ON p.category_id = c.id
LEFT JOIN seller s ON p.seller_id = s.id
ORDER BY id DESC
LIMIT 40 OFFSET #{OFFSET 값};
OFFSET 값 | 0 | 10,000 | 100,000 | 250,000 | 500,000 | 750,000 | 1,000,000 |
---|---|---|---|---|---|---|---|
실행 속도 | 4.8ms | 17.4ms | 2s 508ms | 6s 940ms | 13s 955.5ms | 20s 645.2ms | 25s 328.5ms |
모두 40개의 데이터를 가져오지만 OFFSET 값이 클수록 더 많은 시간이 걸린다는 것을 알 수 있다. 왜냐하면 LIMIT와 OFFSET은 처음부터 모든 값을 우선 가져와서 임시 테이블에 저장한 다음 필요한 만큼만 반환하고 나머지는 버리기 때문이다.
3.1.2 OFFSET 방식의 오해
OFFSET값의 크기를 보면 페이지로 치면 2000페이지가 훌쩍 넘는데 사용자가 이러한 큰 값에 접근할 일이 있을지 의문이 생길 수 있다. 사용자가 Pagination bar의 다음 버튼을 열심히 눌러 2,000페이지를 넘길 일이 없을 것이기 때문이다. 하지만 그건 사실이 아니다.
사용자가 구글 검색으로 서비스의 1,000,000번째 자료(판매 상품, 피드, 포스팅 등)에 접근하였고 이것이 수 십, 수 천, 수 만명에게 공유되고 바이럴된다면 사용자들은 해당 자료에 접근하기 위해 굉장히 오랜 시간이 걸리거나 서버로 응답 받지 못할 수도 있다. 이러한 상황은 서비스를 운영하는 회사에서 가장 피하고 싶은 상황일 것이다. 따라서 OFFSET 방식은 주의할 필요가 있는 구현 방식이다.
3.2 데이터 누락
- 사용자가 1페이지를 요청하고 상품을 구경하고 있다.
- 한편 판매자는 상품 재고가 다 되어 4. 러닝 자켓과 5. 여름 팔토시 상품을 삭제하였다.
- 사용자는 다른 상품을 보기 위해 2페이지를 요청하였다.
- 삭제되지 않았을 경우 2번 페이지를 요청한 결과이다.
Offset 방식은 데이터의 잦은 추가와 삭제가 있을 경우 데이터의 중복과 누락이 발생될 수 있다.
Offset 방식은 전체 데이터 중 Offset만큼 건너 뛰며 이때 이전에 받은 데이터를 고려하지 않고 매번 새로 계산한다. 따라서 3번처럼 총 데이터의 5개의 열을 뛰어넘은 6번부터 10번까지 반환할텐데 이는 2번에서 삭제한 두 개의 데이터를 고려하지 않았기 때문에 2페이지에서 러닝 힙색과 골프화 상품은 누락되는 것이다.
이는 해당 서비스를 재접속하거나 다시 1페이지를 요청하지 않는 이상(새로고침) 다시는 볼 수 없게 된다는 단점이 있다.
3.3 데이터 중복
- 사용자가 1페이지를 요청함
- 한 편 판매자가 등산화와 테니스 채 상품을 등록, 사용자는 기존 데이터를 보고 있음
- 사용자가 2페이지를 요청함, 1페이지에서 이미 보았던 러닝 힙색과 골프화를 중복하여 보게 된다.
- 데이터가 추가되지 않았을 경우의 결과
이러한 경우 사용자는 페이지를 넘어감에도 같은 상품이 보여지는 단점이 있다.
4. Offset 방식의 결론
장점
- 일반적인 방식으로 쿼리가 복잡하지 않다.
- 다양한 정렬 방식을 쉽게 구현할 수 있다.
- 프론트 엔드에서 Pagination bar를 구현할 수 있다.
이러한 장점은 Pagination을 간단하고 빠르게 구현할 수 있다는 장점이 있다.
단점
- 페이지의 뒤로 갈수록 쿼리의 속도가 매우 느려진다.
- 데이터의 잦은 추가와 삭제가 이루어졌을 때 누락과 중복이 발생할 수 있다.
이러한 단점은 실시간으로 빠르게 데이터가 추가 삭제되는 SNS에서는 대단히 오류가 많고 속도가 느릴 것이다. 따라서 자주 바뀌지 않는 적은 양의 데이터에 대한 Pagination이라면 OFFSET 방식이 빠르게 구현할 수 있지만 그렇지 않다면 Cursor 방식의 Pagination을 고려하는 것이 좋다.
5. Cursor 방식의 Pagination 처리
가장 처음 쿼리할 때 아래처럼 where절 없이 쿼리한다.
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
SELECT * FROM product WHERE id > 80 LIMIT 40; //81~120
SELECT * FROM product WHERE id > 120 LIMIT 40;//121~160
SELECT * FROM product WHERE id > 160 LIMIT 40;//161~200
ORDER BY절을 통해 id 내림차순으로 가져올 수도 있다. 그러면 다음 id값은 점점 작아야 하니 비교 연산자를 반대로 해야한다.
//총 자료 수가 200라고 가정한다.
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
SELECT * FROM product WHERE id < 81 ORDER BY id DESC LIMIT 40; //80~41
SELECT * FROM product WHERE id < 41 ORDER BY id DESC LIMIT 40; //40~1
5.1 SQL 작성 시 주의할 점
id는 고유한 값으로 중복이 일어나지 않는다. 만약 중복이 될 수 있는 컬럼을 기준으로 쿼리하게 되면 중복된 값은 모두 생략하고 그 중 마지막 값부터 반환할 수 있다.
1. SELECT id, price FROM product ORDER BY price DESC LIMIT 5;
3. SELECT id, price FROM product WHERE price < 25000 ORDER BY price DESC LIMIT 5;
위 처럼 price라는 중복 가능성 있는 값을 기준으로 정렬하고 기준으로 사용하게 된다면 중복된 데이터들을 생략한다는 문제가 생긴다. 따라서 아래처럼 고유한 id값을 함께 정렬 기준으로 세우고 WHERE절에서 OR, AND과 같은 논리 연산자를 통해 값이 동일할 경우 id값을 참고할 수 있도록 한다.
1. SELECT id, price FROM product ORDER BY price DESC, id DESC LIMIT 5;
3. SELECT id, price FROM product
WHERE price < 25000 OR (price = 25000 AND id < 82 )
ORDER BY price, id DESC LIMIT 5;
이처럼 고유한 값을 통해 price 가격이 같은 경우에 대한 예외 처리를 함으로 기대하는 결과를 가져오게 되었다.
마지막으로 위 쿼리의 결과에서 25,000원 상품의 번호의 순서가 변경된 것을 알 수 있다. 정렬 기준에 id 또한 추가되었기 때문이다. 만약 두 번째 기준으로 삼을 컬럼도 함께 정렬하지 않는다면 같은 가격인 컬럼은 어떻게 정렬될지 모르기 때문에 값이 생략될 수 있다는 것을 주의해야 한다.
6. Offset과 비교한 Cursor 방식의 속도 테스트
Offset 방식과 동일한 방식으로 동일한 값을 반환하도록 하여 테스트 하였다.
OFFSET 값 | 0 | 10,000 | 100,000 | 250,000 | 500,000 | 750,000 | 1,000,000 |
---|---|---|---|---|---|---|---|
실행 속도 | 8.3ms | 8.3ms | 7.1ms | 7.9ms | 7ms | 7.9ms | 7.6ms |
Cursor방식은 아무리 뒤에 위치한 값을 가져와도 기준점이 되는 값까지 뛰어넘고 필요한 부분만 가져오기 때문에 대단히 빠른 방식이다. 실제로 테스트를 해보니 엄청난 차이가 나오는 것을 알 수 있었다.
Offset 방식과 비교해서 Cursor방식은 대단히 속도가 빠르다는 것을 알게 되었다.
7. 최종 결론
구현 방식과 테스트에서 압도적인 성능을 보여주었고 정렬이 복잡해지면 구현이 더 까다로워질 수 있다고 생각하였다. sns와 같은 복잡한 정렬이 필요 없는 대용량 시스템에서는 Cursor 방식이 매우 필수적이라고 느꼈고 온라인 마켓플레이스와 같은 서비스에서는 상품 검색이 자세히 할 수 있으면 상품 개수도 줄기 때문에 Offset방식이 어울릴 수도 있다고 생각하였다. 모든 서비스에 무조건 대입할 수 있는 특별한 방식은 없다고 생각한다. 다만 서비스의 특징과 상황에 따라 올바른 구현 방식을 선택해야 한다고 느꼈다.