docs(keycloak-session-store): import the session-storage lab as a new project

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>
This commit is contained in:
DongHyeonka
2026-09-04 22:51:59 +09:00
co-authored by Claude Opus 5
parent 43bccd08a8
commit b2963105a8
5017 changed files with 372751 additions and 4943 deletions
@@ -0,0 +1,55 @@
---
kind: REFERENCE
slug: a-publicly-readable-state-must-be-complete-by-constraint
title: 공개 읽기 가능한 상태는 완전한 identity를 DB 제약으로 요구한다
topic: fileserver-state-and-fencing
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:a-publicly-readable-state-must-be-complete-by-constraint
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 공개 읽기 가능한 상태는 완전한 identity를 DB 제약으로 요구한다
## 목적
공개 상태에 도달한 레코드가 필수 값을 빠뜨린 채 존재해, 그 값을 전제한 코드가 나중에 실패하는 것을 막는다.
## 규칙
1. 공개 상태의 요구를 데이터베이스 제약으로 표현한다
애플리케이션 검사로 두면 그 검사를 지나지 않는 경로가 언젠가 생긴다.
2. 상태별로 다른 요구를 조건부 제약으로 쓴다
모든 상태에 같은 요구를 걸면 중간 상태를 만들 수 없다.
3. 상태와 버전을 함께 가드한다
상태만 조건에 넣으면 같은 상태에서 출발한 두 전이가 모두 성공한다.
4. 진실의 출처를 하나로 정한다
파일시스템에 바이트가 있다는 것과 공개 가능하다는 것은 다른 사실이다. 어느 쪽이 정본인지 정하고 그것을 문서에 적는다.
## 적용 조건
외부에 노출되는 상태를 갖는 모든 레코드
파일이나 객체처럼 저장소와 메타데이터가 따로 있는 자원
## 예외
내부 처리 단계의 중간 상태는 완전성을 요구하지 않는다. 그 상태가 외부로 새지 않는 것이 보장되어야 한다.
## 예시
파일 메타데이터의 헤더가 관계형 레코드가 공개 가능 여부를 정한다고 명시하고, 모든 상태 전이가 상태와 버전 양쪽으로 가드된다고 적는다.
## 관계
- **파일 상태 기계와 READY가 뜻하는 것**
이 규칙이 나온 개념이다.
- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다**
세 번째 규칙의 일반형이다.
- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다**
같은 원칙의 값 타입 판이다.
@@ -0,0 +1,60 @@
---
kind: REFERENCE
slug: claim-with-a-conditional-update-not-a-read
title: 조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다
topic: fileserver-state-and-fencing
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:claim-with-a-conditional-update-not-a-read
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다
## 목적
읽기와 행동 사이에 상태가 바뀌어, 이미 유효하지 않은 판단으로 되돌릴 수 없는 작업을 수행하는 것을 막는다.
## 규칙
1. 읽고 나서 판단하지 않는다
조건을 갱신문의 where 절에 넣고 갱신 건수로 판단한다.
2. 되돌릴 수 없는 작업 앞에서는 특히 그렇다
물리 삭제나 외부 호출은 되돌릴 수 없다. 그 앞의 판단은 원자적이어야 한다.
3. 양쪽이 같은 사실을 본다
한쪽은 리스의 부재를 보고 다른 쪽은 만료만 보면, 둘 다 자기 기준으로 옳으면서 서로 어긋난다.
4. 상태를 명시적으로 만든다
끝났다는 사실이 값으로 없으면 각 참여자가 그것을 추론하고, 추론의 근거가 서로 다르다.
5. 청구하지 못하면 미룬다
갱신 건수가 0 이면 다른 참여자가 그 행을 들고 있다는 뜻이다. 강제하지 않고 다음 주기로 넘긴다.
## 적용 조건
여러 참여자가 같은 자원을 놓고 경합하는 정리 작업과 배치
물리 삭제나 외부 호출이 뒤따르는 판정
## 예외
읽기와 행동이 같은 트랜잭션 안에서 행 잠금과 함께 일어나면 조건부 갱신 없이도 안전하다. 그 잠금이 실제로 걸리는지 확인해야 한다.
## 예시
정리가 쓰기 리스를 읽어 없음을 확인하고 스테이징 바이트를 지웠다. 읽기와 삭제 사이에 쓰기 작업자가 그 리스를 얻었고, 지워진 것은 업로드가 이어 쓰고 있던 객체였다.
수정 후 정리는 조건부 갱신으로 청구하고, 리스가 실제로 걸려 있으면 아무것도 청구하지 않고 미룬다.
## 관계
- **cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다**
이 규칙을 만든 사례다.
- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다**
같은 규칙의 상태 기계 판이다.
- **시간은 DB에서, 그리고 행을 잠근 다음에 읽는다**
같은 계열의 짝 규칙이다.
@@ -0,0 +1,56 @@
---
kind: REFERENCE
slug: prefix-matching-fits-signatures-not-sniffing
title: 접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다
topic: fileserver-state-and-fencing
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:prefix-matching-fits-signatures-not-sniffing
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다
## 목적
파일 형식 시그니처를 찾는 방법으로 브라우저 스니핑 대상을 찾아, 앞에 바이트를 붙이는 것만으로 우회되는 것을 막는다.
## 규칙
1. 두 문제를 구별한다
형식 시그니처는 정의상 시작 바이트다. 브라우저 스니핑은 관용적 해석이므로 시작이 아니어도 된다.
2. 스니핑 대상은 포함으로 찾는다
앞의 일정 구간 안에 마커가 있으면 탐지한다. 시작이어야 한다는 조건을 걸지 않는다.
3. 앞에 붙는 것들을 목록으로 갖는다
바이트 순서 표시와 널 바이트와 공백과 주석이 흔하다.
4. 탐지 대상은 소비자의 관용도에 맞춘다
무엇을 실행할지 정하는 것은 브라우저다. 우리 파서가 아니다.
5. 같은 함수를 두 목적에 쓰지 않는다
한쪽에 맞추면 다른 쪽이 틀린다.
## 적용 조건
업로드 콘텐츠 검증
인라인으로 제공될 수 있는 모든 콘텐츠의 분류
## 예외
형식 시그니처를 확인해 파일 타입을 판정하는 목적이면 시작 매칭이 옳다. 그 경우 그 판정이 보안 결정으로 쓰이지 않아야 한다.
## 예시
실행 가능 콘텐츠 정책이 앞의 1024 바이트에서 마커를 찾되 시작 매칭을 쓴다. 바이트 순서 표시나 널 바이트나 주석을 앞에 붙이면 탐지되지 않고, 브라우저는 그런 파일도 실행한다.
## 관계
- **scriptable 콘텐츠 탐지가 BOM과 NUL과 주석으로 우회된다**
이 규칙을 만든 사례다.
- **sanitize가 아니라 reject가 기본이다**
탐지된 콘텐츠를 어떻게 다룰지 정한 규칙이다.