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:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+128
@@ -0,0 +1,128 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a01-f002-idfactory-newid
|
||||
title: never-before-used 는 시그니처가 줄 수 없는 보장이다
|
||||
topic: identity-and-value-contracts
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a01-f002-idfactory-newid
|
||||
evidenceCapturedOn: 2026-09-02
|
||||
assets:
|
||||
- key: a01-f002-idfactory-newid
|
||||
file: ../../../final/evidence/rendered/a01-f002-idfactory-newid.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a01-f002-idfactory-newid.txt
|
||||
source:
|
||||
- 분석 문서는 도메인 코어 편 §11 P3 이다. 인터페이스가 저장소 충돌 검사를 요구하지 않고 샘플 테스트도 전역 유일성을 증명하지 않는다는 판정이 그 항목에 있다. 포트와 어댑터의 배선은 §4 에, 어댑터 테스트가 실제로 무엇을 검증하는지는 §9 에 있다.
|
||||
---
|
||||
|
||||
# never-before-used 는 시그니처가 줄 수 없는 보장이다
|
||||
|
||||
식별자 생성 포트의 메서드 문서가 한 번도 쓰인 적 없는 값이라고 적는다. 그 메서드는 인자도 확인 예외도 없어 저장소를 물어볼 수도 충돌을 알릴 수도 없다. 저장소 수준 유일성을 실제로 강제하는 것은 스키마의 기본 키다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **이름은 값이 아니라 registry key다**
|
||||
식별자가 무엇을 보장하는지를 타입 계약으로 적으라는 규칙이다.
|
||||
- **문서가 UUIDv7이라 말하고 생성되는 것은 v4다**
|
||||
다른 리프에서 같은 형태로 나타난 문서와 구현의 거리다.
|
||||
|
||||
## 문제
|
||||
|
||||
포트 파일은 열다섯 줄이고, 메서드 문서는 한 줄이다. 그 한 줄이 충돌 없는 값을 약속한다.
|
||||
|
||||
그 문장은 두 가지로 읽힌다. 확률적 유일성이거나 저장소 수준 유일성이다.
|
||||
|
||||
## 결론
|
||||
|
||||
시그니처가 답을 정한다. T newId() 는 인자를 받지 않고 확인 예외도 선언하지 않는다. 질의할 저장소도 실패를 알릴 반환 통로도 시그니처에 없다.
|
||||
|
||||
값 계약도 같은 쪽을 가리킨다. 포트 문서는 그 값을 36자 canonical 형식으로, 규격은 RFC 9562 의 UUIDv7 로 못박는다. UUID 가 주는 유일성은 확률에 기댄 것이다.
|
||||
|
||||
포트를 직접 구현하는 클래스는 없다. 구현은 샘플이 정의한 하위 포트를 거친다. 작업 로그와 포스터의 팩토리가 각각 포트를 확장하고 타입 파라미터만 채우며, 스프링 컴포넌트인 어댑터 둘이 그것을 구현한다.
|
||||
|
||||
어댑터 쪽 구현 자체는 규격대로다. 밀리초 안에서도 단조 증가하는 생성기를 부른다. 그렇다고 저장소를 확인할 자리가 생기지는 않는다. 오버라이드할 시그니처가 바뀌지 않기 때문이다.
|
||||
|
||||
샘플의 다른 팩토리 둘은 이 포트를 확장하지 않는다. 반환형이 포트의 바운드를 만족하지 못한다. 하나는 문자열을 돌려주고, 다른 하나는 두 값을 한 번에 예약하는 레코드를 돌려준다.
|
||||
|
||||
값 객체가 검사하는 것은 8-4-4-4-12 열여섯진수 모양뿐이다. 버전 자리는 보지 않는다. 계약이 지정한 UUIDv7 조차 타입이 강제하지 않는다.
|
||||
|
||||
저장소 수준 유일성이 없는 것은 아니다. 작업 로그와 포스터에서 식별자 열은 기본 키로 잡혀 있다. 중복 삽입을 막는 것은 이 포트가 아니라 스키마다. 포트만 읽고 유일성이 확보됐다고 보면 틀린다. 그 보장은 데이터베이스에서 빌려 온 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
확인 방식 : 포트 시그니처와 값 계약, 구현 경로 추적, 값 객체의 검사 범위, 마이그레이션의 제약 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 포트 파일 전문을 읽고 메서드의 인자와 예외 선언을 확인한다.
|
||||
2. 그 포트가 만드는 값의 계약 문장을 읽는다.
|
||||
3. 포트를 직접 구현하는 클래스를 센다. 0 이면 하위 포트를 거치는 구현을 찾는다.
|
||||
4. 그 구현이 무엇을 부르는지, 그리고 시그니처가 바뀌었는지 확인한다.
|
||||
5. 값 객체의 정규식이 무엇을 검사하는지 읽는다.
|
||||
6. 그 식별자가 들어가는 테이블의 제약을 마이그레이션에서 찾는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
식별자 생성 포트는 열다섯 줄이다. 그중 13행의 메서드 문서 한 줄이 이 사례의 대상이다.
|
||||
|
||||
## 포트가 줄 수 있는 것은 시그니처가 정한다
|
||||
|
||||
:::evidence key="a01-f002-idfactory-newid" alt="식별자 생성 포트의 파일 전문과 그것이 만드는 값의 계약, 포트를 직접 구현하는 클래스 수와 샘플이 정의한 팩토리 인터페이스 넷, 그중 포트를 확장한 둘을 구현하는 어댑터, 확장하지 않는 둘의 반환형, 실제 구현이 부르는 생성기, 값 객체가 검사하는 정규식, 모듈 README 의 책임 분리, 그리고 식별자 열의 기본 키 제약을 차례로 출력한 터미널 기록." caption="포트 전문 · 값 계약은 36자 canonical UUIDv7 · 직접 구현 0 과 하위 포트 경유 어댑터 둘 · 값 객체는 모양만 검사 · 유일성은 기본 키가 강제 — 73줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
메서드 문서가 새 식별자를 신선하고 한 번도 쓰인 적 없는 값이라고 적는다.
|
||||
|
||||
그 메서드는 인자를 받지 않고 확인 예외도 선언하지 않는다. 이미 쓰인 값인지 물어볼 대상이 없고, 충돌을 알릴 통로도 없다. 저장소 수준 유일성을 약속하려면 둘 중 하나는 있어야 한다.
|
||||
|
||||
## 값 계약이 어느 쪽인지 정한다
|
||||
|
||||
이 포트가 만드는 값의 문서는 36자 canonical UUID 이고, RFC 9562 의 UUIDv7 이라고 버전까지 적는다.
|
||||
|
||||
UUID 의 유일성은 확률적이다. 생성기가 같은 값을 두 번 낼 확률이 무시할 만큼 작다는 뜻이고, 저장소에 그 값이 없다는 확인은 아니다.
|
||||
|
||||
값 객체 쪽은 그보다 더 느슨하다. 정규식이 보는 것은 8-4-4-4-12 열여섯진수 모양뿐이고, 버전 자리는 검사하지 않는다. 생성 어댑터를 거치지 않는 입력 경로에서는 계약이 적은 UUIDv7 조차 강제되지 않는다.
|
||||
|
||||
## 직접 구현은 없고, 구현은 하위 포트를 거친다
|
||||
|
||||
이 포트를 `implements IdFactory<…>` 로 직접 구현하는 클래스는 없다.
|
||||
|
||||
샘플이 팩토리 인터페이스 넷을 정의한다. 그중 둘이 포트를 확장하고, 메서드를 더하지 않은 채 타입 파라미터만 채운다. 나머지 둘은 확장하지 않는데, 반환형이 포트의 바운드 `T extends ResourceId<?>` 를 만족하지 못하기 때문이다. 하나는 문자열을 돌려주고, 다른 하나는 의도 식별자와 연산 식별자를 한 번에 예약하는 레코드를 돌려준다.
|
||||
|
||||
확장한 둘을 스프링 컴포넌트 어댑터가 구현한다. 그 구현은 진짜 UUIDv7 을 만든다.
|
||||
|
||||
```java
|
||||
return WorkLogId.of(UuidCreator.getTimeOrderedEpochPlus1().toString());
|
||||
```
|
||||
|
||||
밀리초 안에서도 단조 증가하고 보안 난수를 쓴다고 어댑터 javadoc 이 적는다. 그래도 저장소를 볼 자리는 생기지 않는다. 오버라이드하는 시그니처가 그대로이기 때문이다.
|
||||
|
||||
## README 는 왜 그렇게 나눴는지를 적는다
|
||||
|
||||
발급할 책임은 도메인이 소유하고, 실제로 만드는 행위는 인프라 어댑터가 수행하며, 애플리케이션 유스케이스가 둘을 조율한다. 그렇게 나누면 도메인이 난수나 시계 같은 구체적 소스를 알지 못한 채 계약만 갖는다는 것이다. 괄호 안의 예시가 UUIDv7 생성기다.
|
||||
|
||||
README 는 책임의 경계를 정할 뿐 보장의 범위를 정하지 않는다. 그 범위를 정하는 것은 시그니처다.
|
||||
|
||||
## 유일성은 있다. 다른 데서 온다
|
||||
|
||||
작업 로그와 포스터의 식별자 열은 기본 키다.
|
||||
|
||||
```text
|
||||
CONSTRAINT pk_work_log PRIMARY KEY (id)
|
||||
CONSTRAINT pk_poster PRIMARY KEY (id)
|
||||
```
|
||||
|
||||
중복 삽입을 거절하는 것은 이 제약이다. 식별자를 자연 키로 쓰거나 중복을 막는 근거로 삼는 코드는 저장소 수준 유일성을 전제하는데, 그 전제를 주는 것은 포트가 아니라 스키마다. 포트를 읽고 유일성을 얻었다고 생각하면, 실제로는 그 보장을 데이터베이스에서 빌려 오고 있다.
|
||||
|
||||
문구를 어떻게 할지는 의도를 먼저 확인해야 정해진다. 확률적 유일성을 뜻한 것이라면 문장을 그 범위로 좁히면 되고, 저장소 수준을 뜻한 것이라면 시그니처가 바뀌어야 한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
저자가 그 문구로 무엇을 의도했는지는 이 저장소가 말하지 않는다. 여기서 확인한 것은 계약이 무엇을 보장할 수 있는가다.
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user