The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.3 KiB
4.3 KiB
id, kind, slug, title, topic, topicName, project, status, studio, questionStatus
| id | kind | slug | title | topic | topicName | project | status | studio | questionStatus |
|---|---|---|---|---|---|---|---|---|---|
| b099ca65-bf9f-4d61-814c-74722453fa3c | QUESTION | nullable-first-highlighted-at | Highlight 없는 FeedItem을 허용할 것인가 | jpa-feed-query-performance | JPA 피드 조회 성능 | Liner N + 1문제 | 게시 전 | https://hyeonworks.com/studio/documents/b099ca65-bf9f-4d61-814c-74722453fa3c/edit | OPEN |
Highlight 없는 FeedItem을 허용할 것인가
정렬키 first_highlighted_at이 nullable이라 하이라이트 없는 FeedItem이 존재할 수 있는 스키마다. 시더는 하이라이트가 만든 FeedItem만 넣어 항상 값이 차지만, 허용 여부를 정하지 않으면 정렬과 커서 비교식의 경계 동작이 정의되지 않는다.
관계
- Keyset Pagination 설계 기준 정렬키의 null 처리가 커서 설계에 걸리는 지점이다.
- Visibility OR이 Keyset Index를 깨뜨린 문제 같은 정렬키 인덱스를 다루는 기록이다.
사실
- 현재 스키마의 first_highlighted_at은 timestamptz nullable이다. NOT NULL이 아니다.
- 시더는 하이라이트가 만든 FeedItem만 생성하므로 이 값을 항상 채운다. 그래서 지금까지의 측정에서는 null이 나타나지 않았다.
- FeedItem은 (user, page) 조합당 하나이고 UNIQUE(user_id, page_id) 제약이 있다.
- keyset 커서는 (first_highlighted_at, id)를 비교식으로 사용한다.
- 정렬키 전용 인덱스는 (first_highlighted_at DESC, id DESC)로 만들었다.
가정
- 하이라이트가 하나도 없는 FeedItem이 생기는 경로가 실제로 있을 수 있다.
- null이 섞이면 커서 비교식이 경계에서 행을 빠뜨리거나 중복시킬 수 있다.
- 정렬 위치를 정의하지 않으면 페이지를 넘길 때 순서가 흔들릴 수 있다.
미지수
- 하이라이트 없는 FeedItem을 만드는 경로가 도메인에 존재하는가. 존재한다면 어떤 상황인가.
- NOT NULL로 좁힐 것인가, null을 허용하고 정렬 위치를 정의할 것인가.
- null을 허용한다면 정렬에서 어디에 두는가. 그 위치를 인덱스가 지원하는가.
- 커서가 null을 만났을 때 비교식을 어떻게 표현하는가.
- 부분 인덱스 조건을 쓴다면 null 행이 인덱스에서 빠지는데 그 행은 어떻게 조회되는가.
- FeedItem 생성 시점과 첫 하이라이트 시점이 다를 수 있는가.
제약
- 결정 시점은 keyset 페이징을 프로덕션에 반영하기 전이다. 커서 비교식과 인덱스 정의가 이 결정에 달려 있다.
- 스키마를 NOT NULL로 좁히려면 기존 데이터에 null이 없어야 한다. 마이그레이션 전에 확인이 필요하다.
- 현재 시더로는 이 경계가 재현되지 않는다. null이 섞인 데이터셋을 따로 만들어야 검증할 수 있다.
선택지
1. NOT NULL로 좁힌다
FeedItem이 항상 하이라이트와 함께 만들어진다면 정렬키를 NOT NULL로 정의한다. 커서 비교식이 단순해지고 인덱스도 그대로 쓸 수 있다.
대신 하이라이트 없는 FeedItem을 만드는 경로가 나중에 필요해지면 스키마와 생성 흐름을 다시 바꿔야 한다.
2. null을 허용하고 정렬 위치를 정의한다
정렬에서 null을 어디에 둘지 명시하고 인덱스도 같은 위치로 만든다. 커서 비교식은 null 구간을 따로 다룬다.
생성 흐름은 자유로워지지만 커서 표현과 인덱스 정의가 복잡해진다.
3. 부분 인덱스로 null 행을 제외한다
정렬키가 있는 행만 인덱스에 담는다. 피드 목록에는 하이라이트가 있는 항목만 노출한다는 정책이 된다.
인덱스는 작아지지만 null 행을 조회하는 별도 경로가 필요하다.
다음 검증
-
도메인에서 하이라이트 없는 FeedItem이 생기는 경로가 있는지 확인한다.
-
기존 데이터에 first_highlighted_at이 null인 행이 있는지 센다.
-
null이 섞인 데이터셋을 만들어 현재 커서 비교식이 경계에서 어떻게 동작하는지 재현한다.
-
세 선택지 각각에서 커서로 넘긴 페이지가 OFFSET 페이지와 같은 행·같은 순서인지 대조한다.
-
정렬 위치를 정의한 경우 인덱스가 그 순서를 그대로 주는지 실행계획으로 확인한다.