- 계약 채택 — schema 2 → 4. 독자 질문, 후보 36건(PROMOTE 24 · MERGE_INTO 8 · KEEP_IN_SSOT 4), 종류별 칸, kind:slug 관계. 저장소 github-project/ca-tmpl @ 761384d (문서가 인용한 IT 7종이 그 커밋에 있다) - 재선별 결과 기존 24건이 전부 살아남았다. 측정이 Reference 안에 들어 있었지만 독립성 검사를 통과하지 못해 Case 로 빼지 않았다 - 평문 칸의 백틱 제거, 「기준선」을 실제 이름으로, frontmatter 에 source·sourceRevision - 리뷰 39건 반영 — 설명 뒤에 붙은 평가·예고·되풀이·독자 오해 가정을 지웠다. 사실을 담은 절반이 있는 문장은 평가만 뺐다 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3.9 KiB
id, kind, slug, title, topic, topicName, topicName, project, status, studio, decisionStatus, sourceRevision, source
| id | kind | slug | title | topic | topicName | topicName | project | status | studio | decisionStatus | sourceRevision | source | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1dbce381-f0dc-4d49-ad68-bd31d205677e | PROJECT_DECISION | keyset-for-feed-pagination | Feed Pagination은 Keyset을 사용한다 | jpa-feed-query-performance | JPA 피드 조회 성능 — N+1 진단과 조회 전략의 진화 | JPA 피드 조회 성능 | Liner N + 1문제 | 게시 전 | https://hyeonworks.com/studio/documents/1dbce381-f0dc-4d49-ad68-bd31d205677e/edit | PROPOSED | n+1liner-lab@2026-08 |
|
id: 1dbce381-f0dc-4d49-ad68-bd31d205677e kind: PROJECT_DECISION slug: keyset-for-feed-pagination title: Feed Pagination은 Keyset을 사용한다 topic: jpa-feed-query-performance topicName: JPA 피드 조회 성능 — N+1 진단과 조회 전략의 진화 topicName: JPA 피드 조회 성능 project: Liner N + 1문제 status: 게시 전 studio: "https://hyeonworks.com/studio/documents/1dbce381-f0dc-4d49-ad68-bd31d205677e/edit" decisionStatus: PROPOSED sourceRevision: n+1liner-lab@2026-08 source:
- final/document.md#14-1
- final/document.md#14-4
Feed Pagination은 Keyset을 사용한다
피드 목록의 페이징은 OFFSET이 아니라 이전 페이지의 마지막 정렬키를 커서로 넘기는 keyset 방식을 쓴다. 정렬키와 같은 컬럼, 같은 방향의 인덱스를 함께 둔다.
근거
- Keyset Pagination 설계 기준 커서에 정렬키를 모두 담고 정렬키·커서·인덱스의 컬럼과 방향을 맞추라는 규칙을 여기서 가져왔다.
- Visibility OR이 Keyset Index를 깨뜨린 문제 깊은 페이지에서 OFFSET이 2,000행을 훑는 동안 keyset은 커서 이후 20행만 읽은 것을 여기서 실행계획으로 확인했다.
- Highlight 없는 FeedItem을 허용할 것인가 정렬 시각이 null인 FeedItem을 허용할지 정하지 않아 커서 비교식이 경계에서 어떻게 동작하는지도 정의되지 않았다.
결정문
피드 목록은 (정렬 시각, 식별자)를 커서로 삼아 그 지점 이후만 조회한다. 정렬키 전용 인덱스를 커서와 같은 컬럼, 같은 방향으로 둔다.
전체 페이지 수를 요구하지 않는 화면에서는 전체 건수를 세는 count 쿼리를 발행하지 않는다.
판단 이유
OFFSET은 정렬 순서에서 앞의 행을 만든 뒤 버리기 때문에, 100번째 페이지에서는 결과 20행을 만들려고 2,000행을 읽었다. 무한 스크롤에서는 페이지를 넘길수록 읽고 버리는 행이 계속 늘어난다.
keyset은 페이지가 깊어져도 읽은 행이 페이지 크기인 20행에서 늘지 않았다. 정렬키 인덱스가 있을 때는 커서 이후 20행만 인덱스에서 읽었다. 읽은 블록은 1개였다.
인덱스를 제거하자 keyset도 2,000행을 훑었다. keyset 문법만으로 읽는 행이 줄어든 것이 아니라 커서와 같은 순서의 정렬키 인덱스가 있어야 했다. 기존 인덱스는 선두 컬럼이 가시성이라 가시성 필터가 없는 이 쿼리에는 쓰이지 않았고, 정렬키만으로 된 인덱스를 따로 만들어야 했다. 그 인덱스는 측정하는 동안 테스트 코드가 만들고 지웠다. 프로덕션에 반영할 때는 V8 마이그레이션으로 추가할 수 있다.
정렬 시각이 같은 행을 안정적으로 넘기려면 식별자까지 커서에 담아야 한다. 시각만 커서로 쓰면 경계에서 행이 빠지거나 중복될 수 있다.
영향
- 임의 페이지로 바로 건너뛸 수 없다. 앞뒤로 이어서 넘기는 탐색만 된다.
- 전체 페이지 수를 화면에 표시하려면 전체 건수를 세는 count 쿼리를 따로 발행해야 한다.
- 정렬 기준마다 인덱스가 하나씩 필요하므로, 정렬 기준을 늘리면 인덱스도 같은 수로 늘고 쓰기마다 갱신할 인덱스가 많아진다.
- 정렬 시각이 null인 행을 허용한다면 정렬에서 그 행을 어디에 둘지와 커서에 무엇을 담을지를 먼저 정해야 한다. 스키마를 정할 때 이 판단을 keyset 페이징 단계 이전에 내리기로 적어 두었지만 아직 정하지 않았다.
- 가시성 조건을 같은 쿼리에 얹자 플래너가 정렬키 인덱스를 쓰지 못하고 세 분기를 각각 스캔해 합친 뒤 다시 정렬했다. 필터를 얹은 페이징은 따로 설계해야 한다.
- 커서는 클라이언트로 나가므로 어떻게 인코딩할지와 위변조를 어떻게 막을지 정해야 한다.