예전에 프로젝트를 진행하던 중 페이징을 구현하던 중 아래와 같은 리뷰가 달렸습니다.
그리고 limit offset 방식의 페이징을 사용하면 어떤 한계가 있는지도 알아보세요~ (지금 상황에서 문제생길 일이 없기 때문에 수정하시라는 건 아닙니다ㅎㅎ)
그래서 limit offset
방식의 한계를 알아보고 이를 개선해 나가는 글을 작성해보고자 합니다.
✓ 참고로 작성하는 글에 관련된 데이터베이스는 MySQL이고 8.0 버전 이상임을 알립니다.
아래와 같은 쿼리가 있을 때 MySQL 에서 동작은 어떻게 될까요?
SELECT id
FROM comment
WHERE is_deleted = FALSE
LIMIT 15
OFFSET 10000;
예상에는 10001 번째 행부터 데이터를 읽기 시작할 것 같지만 실제로는 0번째 행부터 데이터를 읽고 10000 번째 까지의 데이터는 버리게 됩니다.
데이터의 수가 적다면 이는 크게 문제가 되지 않겠지만, 데이터의 수가 많아지면 버리지만 읽어야 하는 행의 개수가 많아져 응답속도가 느려지게 될 것 입니다.
그러면 이를 해결하기 위해서는 OFFSET
을 사용하지 않는 구조로 변경하는 것이 적절해 보입니다.
No offset 방식은 SNS의 더보기 방식과 동일하다고 생각할 수 있습니다. 우리가 핸드폰으로 화면을 스크롤하며 게시물들을 계속해서 볼 수 있는 것을 생각하면 될 것 같습니다.
먼저 no offset
방식의 쿼리를 살펴보겠습니다.
SELECT id
FROM comment
WHERE is_deleted = FALSE AND id < 마지막_조회_ID
LIMIT 15
마지막 조회한 id 부분을 인덱스로 찾아 매번 첫 페이지만 읽도록 하는 방식입니다. 이전에 조회한 결과를 한 번에 건너뛰고 조회하고 싶은 부분부터 조회하게 됩니다.
이를 다른 말로 커서 페이징(cursor pagination) 이라고도 합니다.
Spring Data JPA의 Slice
인터페이스를 떠올려 다음과 같은 클래스를 생성했습니다.
public class Slice<T> {
private static final int HAS_NEXT_DATA_SIZE = 11;
private List<T> data;
private Boolean hasMore;
private Integer nextCursor;
public Slice(List<T> data, Integer cursor) {
if (data.size() == HAS_NEXT_DATA_SIZE) {
this.data = data.subList(0, 10);
this.hasMore = true;
this.nextCursor = cursor;
return;
}
this.data = data;
this.hasMore = false;
this.nextCursor = cursor;
}
}
로직의 흐름은 다음과 같습니다.
hasMore
필드를 true로 설정하고 반환하는 데이터를 10개 담습니다.hasMore
필드를 false로 설정합니다.이후 커서 방식의 페이징을 수행하는 쿼리를 날립니다.
String sql = "SELECT comment.id, user_account.login_id, user_account.profile_url, comment.content, comment.created_at "
+ "FROM comment "
+ "JOIN user_account ON comment.user_account_id = user_account.id "
+ "WHERE comment.issue_id = :issueId AND comment.is_deleted = false AND comment.id >= :cursor LIMIT 11";
굳이 전체 페이지를 나타낼 필요가 없고, 무한 스크롤 방식의 페이징을 구현하고자 한다면 NO-OFFSET 방식의 페이징을 사용하는게 좋을 것 같습니다.
물론 정렬 기준이 다양해진다면 OFFSET 방식의 페이징을 사용하면서 다른 대안을 고려해 봐야겠네요..!