효율적인 데이터 페이징: 키셋 vs 오프셋
Lukas Schneider
DevOps Engineer · Leapcell

소개
웹 애플리케이션과 데이터 중심 시스템의 세계에서 대규모 데이터셋을 효율적으로 표시하는 것은 기본적인 도전 과제입니다. 소셜 미디어 피드, 전자 상거래 제품 카탈로그 또는 로그 파일 뷰어를 상상해 보세요. 이 모든 시나리오는 잠재적으로 수백만 개의 레코드를 가져와 사용자에게 표시하는 것을 포함합니다. 모든 데이터를 한 번에 가져오는 것은 비실용적이고 리소스 집약적이므로 페이징이 사실상의 솔루션으로 등장합니다. 페이징을 통해 방대한 데이터셋을 관리 가능한 청크로 분할하여 성능을 개선하고 로드 시간을 줄이며 사용자 경험을 향상시킬 수 있습니다. 이 글에서는 오프셋 페이징과 점점 더 선호되는 키셋 페이징(커서 페이징이라고도 함)이라는 두 가지 널리 사용되는 페이징 전략을 살펴보고, 확장 가능하고 반응성이 뛰어난 애플리케이션 구축을 위한 기본 원리, 구현 세부 정보 및 실제 적용 사례를 분석합니다.
핵심 개념
각 페이징 방법에 대해 자세히 알아보기 전에 논의 전반에 걸쳐 관련될 몇 가지 핵심 용어를 정의해 보겠습니다.
- 페이지 크기 (제한): 단일 페이지에 대해 검색할 최대 레코드 수입니다.
- 페이지 번호: 현재 페이지의 순차적 순서를 나타내는 정수입니다(예: 페이지 1, 페이지 2, 페이지 3). 일반적으로 오프셋 페이징과 관련이 있습니다.
- 오프셋: 원하는 페이지를 가져오기 전에 데이터셋 시작 부분에서 건너뛸 레코드 수를 나타내는 정수입니다. 이것도 오프셋 페이징과 연결됩니다.
- 커서: 이전 페이지에서 검색된 마지막 레코드의 고유 식별자(종종 기본 키, 타임스탬프 또는 열의 조합)입니다. 이것은 키셋 페이징의 핵심입니다.
- 안정적인 정렬 순서: 데이터를 검색하는 일관되고 예측 가능한 순서입니다. 이는 결과의 정확성과 반복 방지를 보장하기 위해 두 방법 모두에 중요하지만 특히 키셋 페이징에 중요합니다. 일반적으로
ORDER BY
절로 정의됩니다.
오프셋 페이징
오프셋 페이징은 아마도 가장 직관적이고 널리 이해되는 페이징 기법일 것입니다. 이는 특정 수의 레코드(OFFSET
)를 건너뛴 다음 고정된 수의 레코드(LIMIT
)를 검색하는 방식으로 작동합니다.
원리
원리는 간단합니다. N번째 페이지를 가져오려면 첫 (N-1) * LIMIT
개의 레코드를 건너뛴 다음 다음 LIMIT
개의 레코드를 가져옵니다.
구현
id
, name
, price
, created_at
열이 있는 products
테이블을 고려해 보겠습니다.
SELECT id, name, price FROM products ORDER BY id ASC LIMIT 10 OFFSET 0; -- 첫 번째 페이지 (페이지 1, 페이지당 10개 항목) SELECT id, name, price FROM products ORDER BY id ASC LIMIT 10 OFFSET 10; -- 두 번째 페이지 (페이지 2, 페이지당 10개 항목) SELECT id, name, price FROM products ORDER BY id ASC LIMIT 10 OFFSET 20; -- 세 번째 페이지 (페이지 3, 페이지당 10개 항목)
API 맥락에서는 요청이 다음과 같을 수 있습니다: GET /products?page=2&limit=10
. 그러면 백엔드는 OFFSET
을 (page - 1) * limit
로 계산합니다.
장점
- 간단함: 이해하고 구현하기 쉽습니다.
- 임의 액세스: 사용자가 아무 페이지 번호로든 직접 이동할 수 있습니다(예: 100페이지로 이동).
- 총 개수 표시: 항목의 총 개수를 사용할 수 있다면 "X/Y 페이지" 또는 번호가 매겨진 페이지가 있는 페이징 UI를 표시하기 쉽습니다.
단점
- 큰 오프셋에서의 성능 저하:
OFFSET
값이 증가함에 따라 데이터베이스는 더 많은 레코드를 스캔하고 버려야 하므로 쿼리 시간이 크게 느려집니다. 이것은 대규모 데이터셋의 주요 단점입니다. - 레코드 건너뛰기/중복: 사용자가 페이징하는 동안 현재 오프셋 앞의 레코드가 삽입되거나 삭제되면 사용자가 중복 레코드를 보거나 누락된 레코드를 전혀 보지 못할 수 있습니다. 이것은 "팬텀 문제"로 알려져 있으며 일관성 없는 사용자 경험으로 이어질 수 있습니다.
사용 사례
- 비교적 소규모 데이터셋을 가진 관리자 대시보드.
- 데이터 변경으로 인한 간헐적인 불일치가 허용되는 상황.
- 임의 페이지 액세스가 중요한 요구 사항이고 레코드 총수가 과도하게 많지 않은 애플리케이션.
키셋 페이징 (커서 페이징)
키셋 페이징(커서 페이징이라고도 함)은 오프셋 페이징에서 발생하는 성능 및 일관성 문제를 해결하는 보다 강력한 솔루션을 제공하며, 특히 매우 큰 데이터셋의 경우 더욱 그렇습니다.
원리
고정된 수의 행을 건너뛰는 대신 키셋 페이징은 이전 페이지의 마지막 레코드 값을 "커서"로 사용하여 다음 페이지를 가져올 시작점을 결정합니다. 이는 고유하고 정렬된 열 집합( "키셋")을 사용하여 다음 시작점을 정의합니다.
구현
id
가 고유하고 자동 증가하는 기본 키라고 가정하고 products
테이블 예제를 계속 진행해 보겠습니다.
첫 번째 페이지를 가져오려면:
SELECT id, name, price FROM products ORDER BY id ASC LIMIT 10; -- 이 결과 집합의 마지막 레코드 ID가 10이라고 가정합니다.
두 번째 페이지를 가져오려면 (id = 10
이후):
SELECT id, name, price FROM products WHERE id > 10 -- "커서"는 이전 페이지의 마지막 ID입니다. ORDER BY id ASC LIMIT 10;
price
다음 id
로 더 복잡한 정렬이 필요한 경우:
SELECT id, name, price FROM products ORDER BY price ASC, id ASC LIMIT 10; -- 이 페이지의 마지막 레코드가 {id: 7, price: 9.99}라고 가정합니다.
다음 페이지를 가져오려면:
SELECT id, name, price FROM products WHERE (price > 9.99) OR (price = 9.99 AND id > 7) -- price AND id 기준 커서 ORDER BY price ASC, id ASC LIMIT 10;
API 맥락에서 다음 페이지에 대한 요청은 다음과 같을 수 있습니다: GET /products?limit=10&last_id=10
또는 GET /products?limit=10&last_price=9.99&last_id=7
. last_id
또는 last_price
및 last_id
의 조합이 커서 역할을 합니다.
장점
- 일관된 성능: "페이지" 번호가 증가해도 성능이 저하되지 않습니다. 인덱싱된 열(예:
id
또는 키셋의 열 조합)을 사용하는WHERE
절을 통해 데이터베이스는 시작점으로 빠르게 이동할 수 있으므로 페이징 깊이가 많아도 성능이 거의 영향을 받지 않습니다. - 데이터 변경에 대한 견고성: 현재 페이지 이전의 삽입 또는 삭제는 현재 페이지 또는 후속 페이지의 무결성에 영향을 미치지 않습니다. 커서가 정렬된 데이터셋의 특정 지점을 가리키므로 사용자는 중복 레코드를 보거나 보아야 할 레코드를 놓치지 않습니다.
- 확장성: 매우 큰 데이터셋과 트래픽이 많은 애플리케이션에 매우 적합합니다.
단점
- 임의 액세스 불가: 사용자는 임의의 페이지 번호로 직접 이동할 수 없습니다. "다음" 또는 "이전"만 탐색할 수 있습니다 (WHERE 절 및 정렬 순서를 반대로 하여).
- 총 개수 없음: 별도의, 잠재적으로 비용이 많이 드는
COUNT(*)
쿼리 없이 "X/Y 페이지"를 표시하기 어렵습니다. - 안정적인 정렬 순서 필요: 일관되고 고유한 정렬 순서(키셋)가 필수적입니다. 정렬 순서가 고유하지 않으면 키셋에 tie-breaking 열(예: 기본 키)을 추가해야 합니다.
- 복잡한 구현: 특히 복합 키셋이나 "이전 페이지" 기능을 처리할 때 구현하기 까다로울 수 있습니다.
사용 사례
- 사용자가 무한히 스크롤하는 소셜 미디어 피드 (예: Twitter, Facebook).
- 새로운 데이터가 지속적으로 추가되는 로그 탐색 도구.
- 성능과 일관성이 가장 중요한 전자 상거래 제품 목록.
- 사용자가 주로 순차적으로 탐색하는, 대규모로 자주 업데이트되는 데이터셋을 다루는 모든 애플리케이션.
결론
오프셋 페이징과 키셋 페이징 모두 대규모 데이터셋을 관리 가능한 청크로 나누는 목적을 수행하지만, 서로 다른 시나리오에서 뛰어난 성능을 발휘합니다. 오프셋 페이징은 간단함과 페이지 직접 액세스를 제공하여 소규모 데이터셋이나 깊은 페이지에서의 성능이 중요하지 않은 특정 관리 인터페이스에 적합합니다. 그러나 증가하는 오프셋에 따라 성능이 저하되고 동시 수정 시 데이터 불일치에 취약합니다.
반면에 키셋 페이징은 향상된 성능 일관성과 데이터 변경에 대한 견고성을 제공하여 대규모 동적 데이터셋과 높은 확장성과 원활한 사용자 경험이 필요한 사용자 대면 애플리케이션에 선호되는 선택입니다. 임의 페이지 액세스와 단순성을 희생하지만, 효율성과 데이터 무결성의 이점은 현대의 데이터 집약적인 환경에서 이러한 단점을 상쇄하는 경우가 많습니다. 궁극적으로 이 두 방법 간의 선택은 특정 프로젝트 요구 사항, 데이터셋 크기 및 예상 사용자 상호 작용 패턴에 따라 달라집니다. 상당한 데이터를 다루는 대부분의 현대 웹 애플리케이션의 경우 키셋 페이징은 훨씬 더 성능이 뛰어나고 안정적인 사용자 경험으로 이어집니다.