요약
대규모 데이터셋의 경우, 오프셋 기반 페이지네이션 대신 커서 기반 또는 키셋 페이지네이션을 사용하세요. 오프셋 페이지네이션(?page=1&limit=20)은 수백만 개의 레코드에서 성능이 저하되고 데이터 불일치를 초래할 수 있습니다. 모던 펫스토어API는 효율적이고 일관된 결과를 위해 불투명 토큰과 HATEOAS 링크를 사용한 커서 기반 페이지네이션을 구현합니다.
서론
API가 펫 목록을 반환합니다. 데이터베이스에 천만 마리의 펫이 있습니다. 클라이언트가 GET /pets?page=500000&limit=20을 요청합니다. 데이터베이스는 OFFSET 10000000 LIMIT 20을 실행합니다. 쿼리에는 30초가 걸립니다. API는 시간 초과됩니다.
이것이 바로 오프셋 페이지네이션 문제입니다. 작은 데이터셋에서는 잘 작동하지만, 규모가 커지면 문제가 발생합니다. 데이터베이스는 20개의 결과만 반환함에도 불구하고 오프셋에 도달하기 위해 수백만 개의 행을 스캔해야 합니다.
구형 스웨거 펫스토어는 페이지네이션을 전혀 다루지 않습니다. 모던 펫스토어API는 수백만 개의 레코드에 대해 일관된 성능으로 확장 가능한 커서 기반 페이지네이션을 구현합니다.
이 가이드에서는 오프셋 페이지네이션이 실패하는 이유, 커서 기반 페이지네이션이 작동하는 방식, 그리고 모던 펫스토어API가 효율적인 페이지네이션을 구현하는 방법을 배울 것입니다.
규모가 커질 때 오프셋 페이지네이션이 실패하는 이유
오프셋 페이지네이션은 가장 일반적인 접근 방식이지만, 심각한 문제가 있습니다.
오프셋 페이지네이션 작동 방식
GET /pets?page=1&limit=20 → OFFSET 0 LIMIT 20
GET /pets?page=2&limit=20 → OFFSET 20 LIMIT 20
GET /pets?page=3&limit=20 → OFFSET 40 LIMIT 20
데이터베이스는 offset 수만큼의 행을 건너뛰고 limit 수만큼의 행을 반환합니다.
문제 1: 페이지 번호가 커질수록 성능 저하
1페이지:
SELECT * FROM pets OFFSET 0 LIMIT 20;
-- 빠름: 20개 행 스캔
1000페이지:
SELECT * FROM pets OFFSET 20000 LIMIT 20;
-- 느림: 20,020개 행 스캔, 20개 반환
500,000페이지:
SELECT * FROM pets OFFSET 10000000 LIMIT 20;
-- 매우 느림: 10,000,020개 행 스캔, 20개 반환
데이터베이스는 버려지는 행이라도 오프셋까지 모든 행을 스캔해야 합니다. 성능은 페이지 번호에 따라 선형적으로 저하됩니다.
문제 2: 일관성 없는 결과
클라이언트가 결과를 페이지별로 넘기는 동안 데이터가 변경됩니다.
요청 1:
GET /pets?page=1&limit=2
반환: [펫 A, 펫 B]
누군가 펫 Z를 추가합니다 (알파벳순으로 첫 번째 정렬)
요청 2:
GET /pets?page=2&limit=2
반환: [펫 B, 펫 C] ← 펫 B가 두 번 나타납니다!
새로운 펫이 삽입되었기 때문에 펫 B는 두 페이지에 모두 나타났습니다. 반대로 삭제가 발생하면 펫이 건너뛰어질 수도 있습니다.
문제 3: 깊은 페이지네이션은 비용이 많이 듭니다
사용자는 10페이지를 넘어서는 경우가 드뭅니다. 하지만 API가 ?page=1000000을 허용한다면, 이를 처리해야 합니다. 깊은 페이지네이션 쿼리는 비용이 많이 들고 서비스 거부 공격에 사용될 수 있습니다.
오프셋 페이지네이션이 허용되는 경우
오프셋 페이지네이션은 다음의 경우에 잘 작동합니다:
- 작은 데이터셋 (10,000개 레코드 미만)
- 제한된 사용량을 가진 내부 API
- 사용자가 깊은 페이지로 이동하지 않을 관리자 인터페이스
- 자주 변경되지 않는 데이터
공개 API 또는 대규모 데이터셋의 경우 커서 기반 페이지네이션을 사용하세요.
커서 기반 페이지네이션 설명
커서 기반 페이지네이션은 불투명 토큰을 사용하여 결과 세트의 위치를 표시합니다.
작동 방식
요청 1:
GET /pets?limit=20
응답 1:
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9",
"hasMore": true
}
}
요청 2:
GET /pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20
커서는 위치를 인코딩하는 불투명 토큰(일반적으로 base64로 인코딩됨)입니다. 클라이언트는 이를 파싱하지 않고 단순히 다시 전달합니다.
장점
1. 일관된 성능
데이터베이스는 인덱스를 사용하여 커서 위치를 직접 찾습니다:
SELECT * FROM pets
WHERE id > '019b4132-70aa-764f-b315-e2803d882a24'
ORDER BY id
LIMIT 20;
이 쿼리는 데이터셋 내의 위치에 관계없이 빠릅니다. 스캔이 아닌 인덱스 탐색을 사용합니다.
2. 일관된 결과
커서는 안정적입니다. 요청 사이에 데이터가 변경되더라도 일관된 결과를 얻을 수 있습니다. 새 레코드가 중복이나 건너뛰기를 유발하지 않습니다.
3. 깊은 페이지네이션 공격 없음
클라이언트는 임의의 위치로 이동할 수 없습니다. 순차적으로 페이지를 넘겨야 하므로 악용을 제한합니다.
커서 형식
커서는 일반적으로 base64로 인코딩된 JSON입니다:
// 디코딩된 커서
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"createdAt": "2026-03-13T10:30:00Z"
}
커서는 페이지네이션을 재개하는 데 필요한 충분한 정보를 포함합니다. 모던 펫스토어API의 경우, 여기에는 리소스 ID와 정렬 필드가 포함됩니다.
정렬된 데이터에 대한 키셋 페이지네이션
키셋 페이지네이션은 정렬된 데이터에 대한 커서 기반 페이지네이션의 한 변형입니다.
작동 방식
불투명 커서 대신 이전 페이지의 마지막 값을 사용합니다:
요청 1:
GET /pets?limit=20&sortBy=createdAt
응답 1:
{
"data": [
{"id": "...", "createdAt": "2026-03-13T10:00:00Z"},
...
{"id": "...", "createdAt": "2026-03-13T10:30:00Z"}
]
}
요청 2:
GET /pets?limit=20&sortBy=createdAt&after=2026-03-13T10:30:00Z
after 매개변수는 이전 페이지의 마지막 createdAt 값을 사용합니다.
SQL 쿼리
SELECT * FROM pets
WHERE created_at > '2026-03-13T10:30:00Z'
ORDER BY created_at
LIMIT 20;
이는 created_at에 대한 인덱스를 사용하므로 효율적입니다.
키셋 페이지네이션을 사용해야 할 때
- 데이터가 자연스럽게 정렬된 경우 (타임스탬프, ID 등)
- 클라이언트가 페이지네이션 키를 이해해야 하는 경우
- 투명한 페이지네이션을 원하는 경우 (불투명 커서가 아닌)
모던 펫스토어API는 기본적으로 커서 기반 페이지네이션을 사용하지만, 시계열 데이터에 대해서는 키셋 페이지네이션을 지원합니다.
모던 펫스토어API가 페이지네이션을 구현하는 방법
모던 펫스토어API는 HATEOAS 링크를 사용한 커서 기반 페이지네이션을 사용합니다.
요청 형식
GET /pets?limit=20
GET /pets?cursor={token}&limit=20
매개변수:
limit- 페이지당 결과 수 (기본값: 20, 최대: 100)cursor- 이전 응답에서 받은 불투명 페이지네이션 토큰
응답 형식
{
"data": [
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Fluffy",
"species": "CAT"
}
],
"pagination": {
"limit": 20,
"hasMore": true,
"nextCursor": "eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9"
},
"links": {
"self": "https://petstoreapi.com/pets?limit=20",
"next": "https://petstoreapi.com/pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20"
}
}
주요 기능
1. 불투명 커서
커서는 base64로 인코딩됩니다. 클라이언트는 이를 파싱하지 않습니다.
2. HATEOAS 링크
links 객체는 즉시 사용할 수 있는 URL을 제공합니다. 클라이언트는 페이지네이션 URL을 구성할 필요가 없습니다.
3. hasMore 플래그
더 많은 결과가 있는지 여부를 나타냅니다. 클라이언트는 언제 페이지 넘기기를 멈춰야 할지 알 수 있습니다.
4. Limit 유효성 검사
최대 제한은 100입니다. 클라이언트가 너무 큰 페이지를 요청하는 것을 방지합니다.
자세한 내용은 모던 펫스토어API 페이지네이션 문서를 참조하세요.
페이지네이션 응답 형식
모던 펫스토어API는 페이지네이션된 응답을 일관된 구조로 래핑합니다.
컬렉션 래퍼
{
"data": [...],
"pagination": {...},
"links": {...}
}
왜 컬렉션을 래핑하는가?
- 확장성 - 클라이언트를 손상시키지 않고 메타데이터를 추가할 수 있습니다.
- 일관성 - 모든 페이지네이션 엔드포인트는 동일한 형식을 사용합니다.
- HATEOAS - 링크는 클라이언트가 페이지네이션을 탐색하도록 안내합니다.
페이지네이션 메타데이터
"pagination": {
"limit": 20,
"hasMore": true,
"nextCursor": "...",
"totalCount": 1000 // 선택 사항, 계산 비용이 많이 듭니다
}
totalCount는 대규모 데이터셋의 경우 계산 비용이 많이 들기 때문에 선택 사항입니다. 대부분의 클라이언트는 이를 필요로 하지 않습니다.
Apidog로 페이지네이션 테스트
Apidog는 페이지네이션 동작을 포괄적으로 테스트하는 데 도움이 됩니다.
테스트 시나리오
1. 첫 페이지
GET /pets?limit=20
예상: 20개 결과, hasMore=true, nextCursor 존재
2. 다음 페이지
GET /pets?cursor={token}&limit=20
예상: 20개 결과, hasMore=true/false, nextCursor 존재/없음
3. 마지막 페이지
GET /pets?cursor={lastToken}&limit=20
예상: 20개 미만 결과, hasMore=false, nextCursor 없음
4. 빈 결과
GET /pets?status=NONEXISTENT&limit=20
예상: 0개 결과, hasMore=false, nextCursor 없음
5. Limit 유효성 검사
GET /pets?limit=1000
예상: 400 Bad Request (최대 제한 초과)
Apidog 테스트 구성
// 테스트: 페이지네이션 구조
pm.test("응답에 페이지네이션이 있습니다", () => {
pm.expect(pm.response.json()).to.have.property('pagination');
pm.expect(pm.response.json().pagination).to.have.property('hasMore');
});
// 테스트: HATEOAS 링크
pm.test("응답에 링크가 있습니다", () => {
const links = pm.response.json().links;
pm.expect(links).to.have.property('self');
if (pm.response.json().pagination.hasMore) {
pm.expect(links).to.have.property('next');
}
});
올바른 페이지네이션 전략 선택
다양한 전략은 다양한 사용 사례에 적합합니다.
오프셋 페이지네이션
사용 시점:
- 데이터셋이 작을 때 (10,000개 레코드 미만)
- 사용자가 무작위 접근을 필요로 할 때 (50페이지로 이동)
- 데이터가 자주 변경되지 않을 때
- 제한된 사용량을 가진 내부 API
사용하지 말아야 할 때:
- 데이터셋이 클 때 (100,000개 레코드 이상)
- 성능이 중요할 때
- 데이터가 자주 변경될 때
커서 기반 페이지네이션
사용 시점:
- 데이터셋이 클 때
- 성능이 중요할 때
- 데이터가 자주 변경될 때
- 순차적 접근으로 충분할 때
사용하지 말아야 할 때:
- 사용자가 무작위 접근을 필요로 할 때
- 커서 복잡성이 우려될 때
키셋 페이지네이션
사용 시점:
- 데이터가 자연스럽게 정렬될 때
- 투명한 페이지네이션이 선호될 때
- 성능이 중요할 때
사용하지 말아야 할 때:
- 정렬 순서가 복잡할 때
- 여러 정렬 필드가 필요할 때
모던 펫스토어API 권장 사항: 공개 API 및 대규모 데이터셋에는 커서 기반 페이지네이션을 사용하세요.
결론
페이지네이션은 대규모 데이터셋을 반환하는 API에 매우 중요합니다. 오프셋 페이지네이션은 간단하지만 확장성이 떨어집니다. 커서 기반 페이지네이션은 수백만 개의 레코드에 대해 일관된 성능과 안정적인 결과를 제공합니다.
모던 펫스토어API는 불투명 토큰, HATEOAS 링크 및 적절한 메타데이터를 사용하여 커서 기반 페이지네이션을 구현합니다. 이 디자인은 효율적으로 확장되며 훌륭한 개발자 경험을 제공합니다.
Apidog로 페이지네이션 구현을 테스트하여 엣지 케이스를 처리하고, 제한을 검증하며, 일관된 결과를 반환하는지 확인하세요.
핵심 요약:
- 대규모 데이터셋에는 오프셋 페이지네이션을 피하세요.
- 확장성을 위해 커서 기반 페이지네이션을 사용하세요.
- 컬렉션을 메타데이터와 링크로 래핑하세요.
- Apidog로 페이지네이션을 철저히 테스트하세요.
- 모던 펫스토어API의 페이지네이션 패턴을 따르세요.
자주 묻는 질문
페이지네이션 없이 모든 결과를 반환하면 안 되나요?
하나의 응답으로 수백만 개의 레코드를 반환하면 메모리 문제, 느린 네트워크 전송, 좋지 않은 사용자 경험을 초래합니다. 페이지네이션은 대규모 데이터셋에 필수적입니다.
클라이언트가 커서 페이지네이션으로 특정 페이지로 이동할 수 있나요?
아니요, 커서 페이지네이션은 순차적 접근을 요구합니다. 무작위 접근이 필요한 경우, 작은 데이터셋에는 오프셋 페이지네이션을 고려하거나 대신 검색/필터링을 구현하세요.
필터링을 사용한 페이지네이션은 어떻게 처리하나요?
페이지네이션 요청에 필터 매개변수를 포함하세요: GET /pets?status=AVAILABLE&cursor={token}&limit=20. 커서는 위치와 필터 상태를 모두 인코딩합니다.
페이지네이션 응답에 총 개수를 포함해야 하나요?
클라이언트가 필요로 하고 데이터셋이 작은 경우에만 포함하세요. 총 개수를 계산하는 것은 대규모 데이터셋에 대해 비용이 많이 듭니다 (별도의 COUNT 쿼리가 필요합니다).
SQL에서 커서 페이지네이션을 어떻게 구현하나요?
커서 값과 함께 WHERE 절을 사용하세요: SELECT * FROM pets WHERE id > ? ORDER BY id LIMIT 20. 정렬 컬럼에 인덱스가 있는지 확인하세요.
커서 토큰이 무효화되면 어떻게 되나요?
400 Bad Request와 함께 오류 메시지를 반환하세요. 데이터가 삭제되거나 페이지네이션 상태가 만료되면 커서가 무효화될 수 있습니다.
커서는 얼마나 오랫동안 유효해야 하나요?
모던 펫스토어API 커서는 참조된 리소스가 존재하는 한 무기한 유효합니다. 일부 API는 24시간 후에 커서를 만료시킵니다.
여러 정렬 필드를 사용하여 커서 페이지네이션을 사용할 수 있나요?
네, 가능하지만 커서가 모든 정렬 필드를 인코딩해야 합니다. 이로 인해 커서가 더 복잡해집니다. 대신 단일 복합 정렬 키를 사용하는 것을 고려하세요.
