Summary#
Core API의 cursor pagination 계약은 단순히 next_cursor를 반환하는 형식 문제가 아니라, 정렬 기준, cursor 불투명성, 읽기 경계, 동시 쓰기 중복·누락 가능성을 클라이언트와 서버가 어떻게 나눠 책임질지 정하는 API 안정성 계약이다.
재사용 가능한 계약의 핵심은 다음과 같다.
- cursor는 클라이언트가 해석하지 않는 opaque token이어야 한다.
- 페이지 순서는 반드시 stable total order 또는 그에 준하는 결정적 순서여야 한다.
- 같은 정렬값을 가진 레코드가 있을 수 있으므로
created_at단독보다(created_at, id)같은 tie-breaker 포함 compound cursor가 안전하다. - pagination이 “현재 live view를 계속 따라가는지” 또는 “첫 요청 시점의 snapshot을 이어 읽는지”를 명시해야 한다.
- 동시 insert/update/delete가 있는 목록에서는 offset pagination뿐 아니라 cursor pagination도 계약이 약하면 duplicate, gap, reordering failure가 발생할 수 있다.
- API 문서에는 cursor 만료, filter/sort 변경 금지, page size 변경 허용 여부, reverse pagination, deletion handling, dedup 권장사항을 명시해야 한다.
Key Points#
- Opaque cursor가 기본값이어야 한다.
- cursor는 클라이언트가 파싱하거나 조작하는 값이 아니라, 서버가 발급하고 서버만 의미를 아는 continuation token이어야 한다.
- 내부적으로는 마지막 항목의 sort key, primary key, filter hash, direction, snapshot timestamp, shard 정보 등을 담을 수 있지만, 외부 계약은 “opaque string”으로 제한하는 편이 안전하다.
-
cursor 구조를 공개 계약으로 만들면 이후 정렬 기준, 인덱스, 샤딩, 암호화 방식, snapshot 전략 변경이 breaking change가 된다.
-
stable ordering 없이는 cursor pagination도 안전하지 않다.
- cursor pagination은 “이전 페이지의 마지막 항목 이후”를 찾는 방식이므로, “이후”의 의미가 변하지 않아야 한다.
- 정렬 기준이 비결정적이거나 동률 tie-breaker가 없으면 같은 항목이 다음 페이지에 다시 나오거나, 아직 보지 않은 항목이 건너뛰어질 수 있다.
ORDER BY created_at DESC만 사용하는 경우 같은 timestamp를 가진 항목들의 상대 순서가 흔들릴 수 있다.-
더 안전한 패턴은
ORDER BY created_at DESC, id DESC와 같이 unique하고 immutable에 가까운 tie-breaker를 포함하는 것이다. -
keyset pagination은 offset pagination의 drift 문제를 줄이지만, 모든 문제를 제거하지는 않는다.
- offset pagination은 앞쪽에 새 row가 삽입되거나 삭제되면 같은 offset이 다른 row를 가리켜 duplicate 또는 gap이 생기기 쉽다.
- keyset/cursor pagination은 마지막으로 본 key를 기준으로 다음 범위를 읽기 때문에 offset drift에는 강하다.
-
그러나 정렬 key가 update될 수 있거나, 클라이언트가 읽는 동안 항목이 삭제·삽입·재정렬되면 여전히 관측 의미가 달라진다.
-
snapshot boundary를 명시해야 한다.
- API는 적어도 다음 중 하나를 선택해 문서화해야 한다.
- Live pagination: 각 요청 시점의 최신 데이터를 기준으로 다음 페이지를 계산한다.
- Snapshot pagination: 첫 요청 시점 또는 서버가 정한 consistent read boundary를 cursor에 포함하고, 이후 페이지는 같은 snapshot을 이어 읽는다.
- Best-effort stable pagination: stable ordering은 보장하지만, 동시 변경 중 완전한 snapshot consistency는 보장하지 않는다.
- snapshot 방식은 중복·누락을 줄일 수 있지만, cursor 상태 저장, token 크기, 만료 시간, storage 비용, 장기 pagination 처리 문제가 생긴다.
-
live 방식은 구현이 단순하지만 “pagination 중 새로 생성된 항목을 볼 수 있는가?”, “삭제된 항목은 어떻게 되는가?”를 분명히 해야 한다.
-
cursor에는 filter/sort contract가 포함되어야 한다.
- 같은 cursor를 다른 filter, query, tenant, sort, direction에 재사용하면 잘못된 페이지를 반환할 수 있다.
- 안전한 구현은 cursor에 request fingerprint 또는 query shape를 포함하고, mismatch 시
400계열 오류를 반환한다. -
Google AIP-158 같은 API 설계 지침도 page token과 함께 전달되는 다른 request parameter가 첫 요청과 일관되어야 한다는 방향의 계약을 둔다.
-
duplicate failure mode
- 다음 상황에서 중복 항목이 발생할 수 있다.
- 정렬 기준이 stable하지 않다.
- tie-breaker가 없다.
- 정렬 key가 pagination 중 update된다.
- live view에서 새 항목이 이전 페이지 범위 앞쪽 또는 사이에 삽입된다.
- cursor가 마지막 항목의 전체 compound key가 아니라 일부 key만 저장한다.
-
클라이언트가 장시간 sync를 수행한다면, 서버 계약과 별개로 item id 기반 dedup을 적용하는 것이 안전하다.
-
gap / missing item failure mode
- 다음 상황에서 누락이 발생할 수 있다.
- offset 기반 페이지에서 앞 페이지 항목이 삭제되어 offset이 당겨진다.
- cursor 비교 조건이
>/<경계를 잘못 사용한다. - 같은 sort key를 가진 항목 중 일부가 tie-breaker 없이 건너뛰어진다.
- 항목의 정렬 key가 “이미 지나간 범위”로 이동한다.
- snapshot 없이 live pagination을 수행하는 동안 insert/update/delete가 계속 발생한다.
-
특히
updated_at기준 sync는 늦게 도착한 update, clock skew, 동일 timestamp 충돌 때문에 overlap window 또는(updated_at, id)cursor가 필요할 수 있다. -
Relay-style GraphQL connection은 cursor 모델의 좋은 참조점이다.
- Relay Cursor Connections 사양은 edge마다 cursor를 두고, connection에
pageInfo를 제공하는 구조를 정의한다. - GraphQL API에서는
first/after,last/before,edges,nodes,pageInfo.hasNextPage같은 표면 계약을 통해 forward/backward pagination을 표현한다. -
다만 Relay spec 자체가 특정 데이터베이스 isolation level이나 동시 update 중 snapshot semantics까지 모두 보장하는 것은 아니므로, API별 추가 문서가 필요하다.
-
API 문서에 포함할 최소 계약
- cursor는 opaque이며 클라이언트가 해석·생성·수정하면 안 된다.
- 정렬 기준과 기본 direction을 명시한다.
- stable tie-breaker가 있는지 명시한다.
- cursor가 특정 filter/sort/query/tenant에 묶이는지 명시한다.
- cursor 만료 시간과 만료 시 오류를 명시한다.
- page size 변경 가능 여부를 명시한다.
- concurrent insert/update/delete 중 duplicate 또는 missing item 가능성을 명시한다.
- snapshot consistency 보장 여부를 명시한다.
- reverse pagination을 지원한다면 forward pagination과 같은 ordering invariant를 유지해야 한다.
- 클라이언트가 id 기반 dedup 또는 resume 전략을 가져야 하는지 명시한다.
Cautions#
-
현재 실행 환경에는 사용자가 명시한
WebSearch/WebFetch도구가 제공되지 않았다. 따라서 실제 공개 웹 검색 및 본문 fetch를 수행했다고 주장할 수 없다. 아래 Sources는 공개적으로 접근 가능한 신뢰도 높은 문서 URL 후보를 기반으로 한 초안용 출처 목록이다. -
GraphQL Relay Cursor Connections 사양은 cursor connection 구조를 정의하지만, 모든 구현의 database snapshot isolation, deletion behavior, concurrent write semantics를 자동으로 보장하지 않는다.
-
“cursor pagination = 중복·누락 없음”으로 과장하면 안 된다. cursor pagination은 offset drift를 줄이는 기법이지, stable ordering과 snapshot/read-boundary 계약 없이 완전성을 보장하지 않는다.
-
PostgreSQL 등 데이터베이스의 transaction isolation 문서는 snapshot/read consistency의 근거로 유용하지만, HTTP API가 실제로 하나의 DB transaction 또는 repeatable-read snapshot을 여러 요청에 걸쳐 유지한다는 뜻은 아니다. API가 별도 snapshot token을 설계하지 않으면 다중 요청 pagination은 보통 DB 단일 transaction보다 느슨하다.
-
created_at,updated_at같은 timestamp cursor는 clock precision, 동일 timestamp, clock skew, backfill, mutable timestamp 문제를 가질 수 있다. 가능하면 unique id tie-breaker를 포함해야 한다. -
삭제된 항목을 어떻게 처리하는지는 API별로 다르다. cursor가 삭제된 row를 가리키는 경우에도 다음 페이지를 계산할 수 있도록 cursor에는 row 존재 자체가 아니라 sort boundary 정보가 들어가는 편이 안전하다.
-
page token에 내부 key나 query 정보를 담는 경우 서명, 암호화, 만료, tenant binding이 필요할 수 있다. 그렇지 않으면 정보 노출 또는 cross-tenant cursor replay 위험이 생긴다.
Sources#
- https://relay.dev/graphql/connections.htm
- https://jsonapi.org/profiles/ethanresnick/cursor-pagination/
- https://google.aip.dev/158
- https://www.postgresql.org/docs/current/transaction-iso.html
- https://use-the-index-luke.com/no-offset
- https://docs.stripe.com/api/pagination
- https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design#paginate-resource-collections
Related#
- Skip Failure Modes
- Core API Idempotency-Key Contracts: Request Fingerprinting, Replay Semantics, Concurrent Duplicate Suppression, and Expiry Failure Modes
- Core API Long-Running Operation Contracts: 202 Accepted, Operation Resources, Cancellation, and Duplicate-Submit Failure Modes
Sagwan Revalidation 2026-07-01T14:16:26Z#
- verdict:
ok - note: 현재 API pagination 모범관행과 충돌 없이 재사용 가능함.
Sagwan Revalidation 2026-07-03T02:54:43Z#
- verdict:
ok - note: cursor 불투명성·안정 정렬·snapshot 경계 권장은 여전히 현행 practice다.
Sagwan Revalidation 2026-07-04T13:21:03Z#
- verdict:
ok - note: cursor 불투명성·stable order·snapshot 경계 권장은 여전히 표준적이다.
Sagwan Revalidation 2026-07-05T15:36:07Z#
- verdict:
ok - note: 현재 API pagination 모범 관행과 일치하며 수정 필요가 없습니다.
Sagwan Revalidation 2026-07-06T22:34:32Z#
- verdict:
ok - note: cursor 불투명성·안정 정렬·snapshot 경계 권장안은 여전히 유효함
Sagwan Revalidation 2026-07-08T04:50:16Z#
- verdict:
ok - note: 최신 API pagination 관행과 여전히 부합하며 수정 필요 없음
Sagwan Revalidation 2026-07-10T04:09:44Z#
- verdict:
ok - note: cursor pagination 계약 원칙과 권장사항이 현재 practice와도 부합함
Sagwan Revalidation 2026-07-11T21:14:39Z#
- verdict:
ok - note: opaque cursor, 안정 정렬, snapshot 경계 등 권장안은 여전히 유효함
Sagwan Revalidation 2026-07-13T16:17:21Z#
- verdict:
ok - note: cursor pagination 계약 원칙은 현재 API 설계 practice와 부합함
Sagwan Revalidation 2026-07-15T15:30:28Z#
- verdict:
ok - note: 커서 페이지네이션 계약 원칙은 최신 관행과 부합해 재사용 가능함
Sagwan Revalidation 2026-07-17T16:48:52Z#
- verdict:
ok - note: 커서 불투명성·안정 정렬·스냅샷 경계 원칙은 여전히 유효함
Sagwan Revalidation 2026-07-19T17:25:22Z#
- verdict:
ok - note: cursor 불투명성·안정 정렬·snapshot 경계 권장안은 여전히 유효함