PAGINATION

백엔드 개발자의 Pagination 모험 : 어떻게 골라야 할까?

김문범 프로필 이미지

김문범

2023.12.11

6 min read

백엔드 개발자의 Pagination 모험 : 어떻게 골라야 할까?

안녕하세요! 코멘토에서 백엔드 개발자로 일하고 있는 김문범입니다. 이번에는 제가 코멘토에 적용해보려고 테스트 중인 기술에 대해서 서술해보려고 합니다.

그 주제는?!

제가 적용해보려는 기술은 “Pagination”에 대한 방식입니다. 여러분은 PHP Pagination에 몇가지의 종류가 있다고 알고 계신가요? 저는 2가지를 알고 있었지만 그 방식에 대한 정확한 명칭은 잘 모르고 있었습니다. 그러나 이번에 테스트를 진행하면서 Pagination을 좀 더 정확히 알 수 있게 되었습니다.

Pagination의 종류

PHP에서 자주 쓰이는 Pagination은 3가지가 있습니다.

  1. Offset Pagination
    - Limit(페이지 당 개수) / Offset(어디서부터 시작하여 데이터를 가져올 것인가?)을 이용한 페이징 방식입니다.
  2. Offset Pagination With Deferred Joins
    - 지연 조인(Deferred Joins)을 적용한 Offset Pagination은 index를 이용하여 데이터 목록에 대한 접근을 최소화하는 쿼리 최적화의 방법입니다.
  3. Cursor Pagination
    - Limit를 이용하지만 Offset이 없어서 단순하게 “이전 페이지와 다음 페이지”만 이동할 수 있는 페이징 방식입니다.

3가지 Pagination을 쿼리로 표현하면 아래와 같습니다. SQL은 저희가 사용하는 MySQL 기준입니다.

-- Offset Pagination
select * from data_table order by id limit 20 offset 1000000;

-- Offset Pagination With Deferred Joins
select * from data_table
    inner join (select id from data_table order by id limit 20 offset 1000000) as dt using (id)
order by id;

-- Cursor Pagination
select * from data_table where id > 999999 order by id limit 20;

각 방식의 성능을 비교하면 Cursor Pagination > Offset Pagination With Deferred Joins > Offset Pagination의 순으로 Cursor Pagination이 가장 훌륭합니다.

그래서 Cursor Pagination을 적용?!

로컬 환경 기준으로 기존 Offset Pagination 방식으로 약 0.4 ~ 0.5초의 시간이 걸리는 작업이 Offset Pagination With Deferred Joins을 사용한다면 약 0.2초로 줄어들고 Cursor Pagination을 이용하게 되면 약 0.0004초로 큰 폭으로 줄어들었습니다. 하지만 Cursor Pagination은 리스트의 합계를 사용하지 못하는 단점이 있습니다. 그래서 이전 또는 다음 페이지만 이동할 수 있는 곳에서만 사용하거나 무한 스크롤 시에 사용하면 좋을 것 같다고 생각했습니다. 이번에 적용하는 곳에서는 리스트의 합계가 필요하기 때문에  합계를 계산 할 수 있는 Offset Pagination With Deferred Joins을 이용해봐야겠다고 생각했습니다.

하지만 Offset Pagination With Deferred Joins 사용 시에는 주의사항이 있습니다. 바로 인덱스가 된 칼럼을 사용하셔야 합니다. 제대로 사용하지 않으면 오히려 더 느려질 수 있기 때문에 인덱스 구조 설계가 매우 중요합니다. 하지만, 현재 서비스에서 구현이 어려워서 “혹시 패키지가 있을까?”라는 생각에 검색을 시작했습니다.

Fast⚡️Pagination란?

Fast Pagination은 지연 조인과 유사한 방식의 Query를 사용하는 Pagination 패키지입니다.

-- github에서 소개한 예시
select * from contacts      -- The full data that you want to show your users.
where contacts.id in (      -- The "deferred join" or subquery, in our case.
    select id from contacts -- The pagination, accessing as little data as possible - ID only.
    limit 15 offset 150000      
)

Fast⚡️Pagination 소스코드 뜯어보기!

해당 패키지의 중심이 되는 소스코드라고 생각이 되는 부분을 해석해보겠습니다.

/*
 * File : src/FastPaginate.php
 * Line : 73 ~ 95
 */
// Primary Key만 선택하여 모든 릴레이션을 없이 Offset Pagination
$paginator = $this->clone()
  ->select($innerSelectColumns)
  ->setEagerLoads([])
  ->{$paginationMethod}($perPage, ['*'], $pageName, $page);

// 위의 페이지네이션에서 PK값만 배열로 정리한다.
$ids = $paginator->getCollection()->map->getRawOriginal($key)->toArray();

// PK값의 타입에 따라서 쿼리가 변경된다.
if (in_array($model->getKeyType(), ['int', 'integer'])) {
  $this->query->whereIntegerInRaw("$table.$key", $ids);
} else {
  $this->query->whereIn("$table.$key", $ids);
}

// Cursor Pagination으로 해당 페이지의 데이터를 가져온다.
$items = $this->simplePaginate($perPage, $columns, $pageName, 1)->items();

즉, 위의 소스 코드를 설명한다면 “PK만을 선택하여 Pagination을 구성하여 해당 페이지에 PK를 이용하여 나머지 데이터를 가지고와서 다시 Pagination의 형태로 만들어준다”입니다.

또 그래서 Fast⚡️Pagination를 적용했을까?

정답은 아닙니다. MySQL 8.0.24 이상의 버전이 아니라면 subquery에 LIMIT & IN/ALL/ANY/SOME과 같은 쿼리를 사용할 수 없습니다. 현재 코멘토의 경우는 MySQL버전이 8.0.24가 아니라서 Fast Pagination을 적용하더라도 Deferred Joins(지연조인)을 이용하는 방법이 아니고 유사한 방식으로 작동방식이 변경됩니다. 그렇다면 굳이 Fast Pagination을 사용할 이유가 사라집니다.

후기

비록 즉시 새로운 Pagination을 적용을 해볼 수는 없었지만 지금까지 페이지네이션 테스트를 통해서 배운 것들을 참고해서 앞으로 기회가 된다면기존의 Pagination을 더 성능이 좋은 Offset Pagination With Deferred Joins으로 바꿔보고 가장 성능이 좋은 Cursor Pagination을 도입해볼 수 있는 무한 스크롤 또는 단순하게 이전 페이지와 다음 페이지만을 이동하는 부분에 적용해보려고 합니다.
여러분에게도 많은 도움이 되셨으면 좋겠습니다.
그럼 여기까지 코멘토의 백엔드 개발자 김문범이었습니다! (*^▽^)/

참고자료


커리어의 성장을 돕는 코멘토에서 언제나 함께 성장할 개발자를 기다리고 있습니다. 채용 페이지에서 코멘토가 어떤 회사인지, 어떤 사람을 찾는지 더 자세히 확인해보세요. 😊