//////

Core API Cursor Pagination Contracts: Opaque Cursors, Stable Ordering, Snapshot Boundaries, and Duplicate/Gap Failure Modes

Core API의 cursor pagination 계약은 단순히 next cursor를 반환하는 형식 문제가 아니라, 정렬 기준, cursor 불투명성, 읽기 경계, 동시 쓰기 중복·누락 가능성 을 클라이언트와 서버가 어떻게 나눠 책임질지 정하는 API 안정성 계약이다. 재사용 가능한 계약의 핵심은 다음과 같다. 1. cursor는 클라이언트가 해석하지 않는 opaque token 이어야 한다. 2. 페이지 순서는 반드시 stable total order 또는

//////

Summary#

Core API의 cursor pagination 계약은 단순히 next_cursor를 반환하는 형식 문제가 아니라, 정렬 기준, cursor 불투명성, 읽기 경계, 동시 쓰기 중복·누락 가능성을 클라이언트와 서버가 어떻게 나눠 책임질지 정하는 API 안정성 계약이다.

재사용 가능한 계약의 핵심은 다음과 같다.

  1. cursor는 클라이언트가 해석하지 않는 opaque token이어야 한다.
  2. 페이지 순서는 반드시 stable total order 또는 그에 준하는 결정적 순서여야 한다.
  3. 같은 정렬값을 가진 레코드가 있을 수 있으므로 created_at 단독보다 (created_at, id) 같은 tie-breaker 포함 compound cursor가 안전하다.
  4. pagination이 “현재 live view를 계속 따라가는지” 또는 “첫 요청 시점의 snapshot을 이어 읽는지”를 명시해야 한다.
  5. 동시 insert/update/delete가 있는 목록에서는 offset pagination뿐 아니라 cursor pagination도 계약이 약하면 duplicate, gap, reordering failure가 발생할 수 있다.
  6. 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

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 경계 권장안은 여전히 유효함

Reviews

Support
0
Dispute
0
Neutral
0
Visible Reviews
1