Files
document-haness/docs/clean-architecture-backend-template/analysis/01-domain-core.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 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>
2026-09-04 22:51:59 +09:00

261 lines
17 KiB
Markdown

# domain-core 상세 분석
## SSOT identity — 2026-08-31 재검증
- registered leaf id: `domain-core`
- canonical state `analysisFile`: `analysis/01-domain-core.md` (이 문서) — 이 leaf의 단일 SSOT
- source path: `src/domain-core` · Gradle `:domain-core`
- registry `allowed_dependencies`: **`[]`**
- registry `runtime_memberships`: `["app-bootstrap", "sample-portfolio"]`
- coverage ledger: `FULL_READ` **10** / `STRUCTURAL_ONLY` **3** / `EXCLUDED` **1** / `UNCLASSIFIED` **0**
- 최초 분석 revision `a24ece9c` → 재검증 revision `21234e38` · 이 리프의 변경 파일 **0**
- 재검증 증거: `EVD-333`(소스 드리프트 0), `EVD-334`(lane 재실행)
> 재검증이 확인한 것은 대상이 움직이지 않았다는 사실이지, 아래 서술이 옳다는 보증이 아니다.
> 이번 사이클에서 코드에 대고 다시 확인한 항목은 이 문서의 검증 절과 위 증거가 가리키는 범위다.
---
## 분석 범위와 결론 상태
- **revision:** `a24ece9cf797f7ea647e33bf846b115208ed1ba5`
- **Gradle path:** `:domain-core`
- **registry source path:** `src/domain-core`
- **allowed project dependencies:** 없음
- **runtime memberships:** `app-bootstrap`, `sample-portfolio`
- **scope status:** COMPLETE
이 문서는 `domain-core` 자체의 모든 production Java와 module-local build/docs를 읽고, 이 모듈이 제공하는 식별자 계약과 도메인 stereotype marker가 실제로 어디서 소비되고 어떤 build-time rule로 강제되는지까지 추적한 bounded analysis다. `sample-portfolio`의 구체 도메인 모델 전체나 `app-bootstrap` 전체는 이 scope의 소유 대상이 아니며, reachability/enforcement를 증명하는 관련 부분만 읽었다.
## 1. Quantified scope map
### Owned source
- production Java: **7 files / 107 LOC** (`package-info.java` 2개 포함)
- module build file: **1** (`build.gradle`)
- module docs/instructions: **2** (`CLAUDE.md`, `README.md`)
- dependency lock: **1** (`gradle.lockfile`)
- Java tests: **0**
- test placeholder: `.gitkeep` 2개
- production packages: `dev.caskeleton.domain`, `.identifier`, `.stereotype`
- public domain contracts: `IdFactory`, `ResourceId`
- runtime-retained markers: `AggregateRoot`, `DomainEvent`, `ValueObject`
`build.gradle``dependencies {}`는 비어 있다. production Java import도 `java.lang.annotation.*` 외 제3자/framework import가 없다. 따라서 현재 source shape 자체는 module instruction의 “pure domain layer”와 일치한다.
## 2. Coverage ledger
| item/group | disposition | reason |
|---|---|---|
| `CLAUDE.md` | FULL_READ | module policy SSOT |
| `README.md` | FULL_READ | explicit design rationale |
| `build.gradle` | FULL_READ | declared dependency surface |
| `gradle.lockfile` | STRUCTURAL_ONLY | inherited build/check/test tooling dependency lock; production dependency edge를 추가하지 않음 |
| `identifier/IdFactory.java` | FULL_READ | public identity generation port |
| `identifier/ResourceId.java` | FULL_READ | public resource-id contract |
| root `package-info.java` | FULL_READ | domain package anchor |
| `stereotype/AggregateRoot.java` | FULL_READ | modeling marker |
| `stereotype/DomainEvent.java` | FULL_READ | modeling marker |
| `stereotype/ValueObject.java` | FULL_READ | modeling marker |
| stereotype `package-info.java` | FULL_READ | marker semantics |
| two test `.gitkeep` files | STRUCTURAL_ONLY | no executable test content |
| generated `build/` tree | EXCLUDED | source/build-output ownership evidence상 generated artifact; current source contract denominator에서 제외 |
Owned source 기준 unclassified relevant item은 **0**이다.
## 3. 이 모듈이 실제로 소유하는 것
### 관찰: 재사용 가능한 도메인 “내용”보다 도메인 모델링 계약을 소유한다
현재 `domain-core`에는 WorkLog 같은 실제 aggregate가 없다. 실제 샘플 aggregate/value object/event는 `sample-portfolio`에 있다. 이 모듈에 남은 production surface는 다음 두 종류다.
1. **식별자 추상화**`ResourceId`, `IdFactory`
2. **모델링 표식**`@ValueObject`, `@AggregateRoot`, `@DomainEvent`
따라서 “business concepts, entities, value objects…”를 둘 수 있는 계층이라는 정책과 달리, 현재 snapshot의 실제 contents는 skeleton 전반에서 사용할 **domain-layer contract/marker**에 가깝다. 이는 현재 source에 대한 관찰이며, 향후 실제 production domain type이 이 module에 추가되지 않는다는 뜻은 아니다.
## 4. Identifier contract
### `ResourceId<SELF>`
`ResourceId`는 F-bounded generic marker이며 public surface는 `String value()` 하나다. README에 기록된 명시적 rationale은 실제 ID 구현이 `sample-portfolio`에 있으므로 `sealed permits`로 닫으면 `domain-core -> sample-portfolio` 역방향 의존이 생긴다는 것이다. 현재 registry에서도 `domain-core.allowed_dependencies=[]`이고, `ModuleRegistry`는 production module이 `sample-portfolio`를 allowlist에 넣는 것 자체를 거부한다. root `verifyCleanArchitectureDependencies`도 실제 project edge가 registry allowlist를 넘으면 실패한다.
따라서 **unsealed 선택의 이유는 문서와 build policy가 서로 일치한다**.
다만 `ResourceId.value()`의 Javadoc/README는 “36-character canonical UUID / RFC 9562 UUIDv7”를 계약처럼 서술하지만 interface 자체는 이를 검증하지 않는다. 실제 샘플 `WorkLogId`도 현재 regex로 `8-4-4-4-12` hex shape만 검사하며 UUID version nibble이 7인지, RFC variant인지 검사하지 않는다. property test `acceptsEveryCanonicalUuid`는 오히려 임의의 32 hex를 hyphenate한 모든 값을 허용한다고 명시적으로 검증한다.
**관찰 결과:** “UUID-shaped canonical string”은 현재 검증되지만 “반드시 UUIDv7”이라는 더 강한 서술은 생성 adapter 경로에서는 성립해도 모든 `WorkLogId.of(...)` 입력 경로의 불변식으로는 강제되지 않는다.
### `IdFactory<T extends ResourceId<?>>`
`IdFactory``newId()` 하나를 가진다. sample에서는 `WorkLogIdFactory extends IdFactory<WorkLogId>`로 specialization하고, `UuidWorkLogIdFactory`가 Spring `@Component` adapter로 구현한다. adapter는 `UuidCreator.getTimeOrderedEpochPlus1()`을 호출하고 application use case가 factory를 주입받는 구조가 확인된다.
즉 source dependency는 안쪽의 domain port를 바깥 adapter가 구현하는 방향이며, domain-core는 concrete UUID library/Spring을 모른다.
`newId()` Javadoc의 “never-before-used”는 타입/저장소 확인으로 강제되는 보장은 아니다. 현재 adapter test는 연속 두 값의 distinctness와 1,000회 monotonic ordering을 확인한다. 이 표현은 생성 전략의 기대 계약이지 전역 uniqueness를 저장소와 대조해 증명하는 메커니즘으로 읽어서는 안 된다.
## 5. Stereotype markers와 invariants
세 annotation은 모두 `@Target(TYPE)`, `@Retention(RUNTIME)`, `@Documented`인 framework-neutral marker다.
### `@ValueObject`
marker 자체는 불변성을 구현하지 않는다. `CleanArchitectureTest`가 annotation 대상 또는 `..domain.vo..` package type에 public no-arg constructor가 없어야 한다고 강제한다. sample의 `WorkLogId`, `Period`, `WorkLogOwner`, `PosterId` 등이 실제 production consumer다.
따라서 marker의 의미는 **“이 annotation을 붙이면 ArchUnit guardrail의 subject가 된다”**는 build-time qualification이다. 실제 field 불변성/defensive copy 등 모든 value-object 속성을 자동 검증하는 것은 아니다.
### `@AggregateRoot`
sample의 `WorkLog`, `Poster`가 실제 production consumer다. `CleanArchitectureTest``set.*` 이름의 method가 public이면 실패시킨다. README와 test description 모두 이 rule이 이름 패턴 밖의 mutator(`applyXxx` 등)는 포착하지 못한다고 명시한다.
따라서 이 marker는 aggregate consistency를 자동으로 보장하는 annotation이 아니라 **특정 위험 surface(public raw setter)를 정적으로 제한하는 qualification marker**다.
### `@DomainEvent`
sample의 `WorkLogReserved`, `PosterCreated/Archived/...` 등이 사용한다. ArchUnit은 annotation type이 record인지 검사하고 Kafka/Spring HTTP/JAX-RS package dependency를 금지한다. production `LiveEventStompBroadcaster`는 runtime reflection으로 event class가 `@DomainEvent`인지 검사하므로 `RUNTIME` retention은 ArchUnit 외 실제 runtime consumer에도 필요하다.
transport-free rule의 forbidden package list는 exhaustive transport taxonomy가 아니다. test 설명 자체가 “새 broker/transport가 도입되면 list를 확장”해야 하는 구현상 한계를 명시한다.
## 6. Purity / dependency enforcement
### source-level observation
현재 domain-core production code는 Java standard annotation API 외 외부 import가 없다. module `dependencies {}`도 비어 있다.
### project-edge enforcement
`src/settings.gradle``ca.architecture-registry` settings plugin을 통해 `modules.json`을 읽고 각 registered leaf를 include/mapping한다. `ModuleRegistry`는 다음을 settings time에 fail-closed 검증한다.
- root/module field set 정확성
- nonblank id/path
- duplicate id/Gradle path/canonical source directory
- source path가 repository root 밖으로 escape하지 않음
- runtime composition 값
- self dependency
- unknown allowed dependency
- production module의 `sample-portfolio` 허용 금지
root `verifyCleanArchitectureDependencies`는 이후 실제 Gradle project dependencies를 registry allowlist와 비교한다. 즉 domain-core에 project dependency가 추가되면 `allowed=[]`과 불일치해 verification failure가 된다.
### class dependency enforcement
`CleanArchitectureTest.DOMAIN_IS_PURE``..domain..` classes가 Spring/JPA/Hibernate/Lombok/application/adapter/bootstrap/service/infra/presentation/cmd 계열에 의존하지 못하게 한다. 별도 `DOMAIN_HAS_NO_LOGGER`도 logging framework dependency를 금지한다.
중요한 구분은 이 ArchUnit rule이 **domain-core module만이 아니라 package name에 `domain`이 들어가는 production classes 전체**를 subject로 한다는 점이다. 따라서 sample-portfolio domain model도 같은 purity/modeling guardrail의 대상이다.
## 7. Runtime reachability / wiring
`domain-core` 자체에는 Spring bean/configuration/entry point가 없다. Registry상 `app-bootstrap`, `sample-portfolio` 두 runtime composition에 membership이 있고, concrete consumers가 compile-time type/annotation으로 이 module을 참조한다.
- `ResourceId`: application-core messaging contract 및 sample IDs에서 참조
- `IdFactory`: sample factory/use-case/identifier adapter에서 참조
- `AggregateRoot`: sample aggregate에서 사용
- `DomainEvent`: sample events와 websocket broadcaster qualification에서 사용
- `ValueObject`: sample IDs/value objects에서 사용
따라서 major public abstraction이 완전히 dead/unwired인 상태는 아니다. 반대로 `domain-core`가 runtime service를 직접 수행한다는 근거도 없다.
## 8. Success / failure mechanics
이 module의 runtime executable behavior는 매우 작다. annotation 자체에는 success/failure path가 없고, interface도 implementation을 가지지 않는다. 주요 failure mechanics는 **build-time architecture violation**이다.
- forbidden framework/domain dependency → `DOMAIN_IS_PURE`
- domain logger dependency → `DOMAIN_HAS_NO_LOGGER`
- public no-arg value object → `VALUE_OBJECTS_HAVE_NO_PUBLIC_NO_ARG_CONSTRUCTOR`
- public `set*` aggregate mutator → `AGGREGATE_ROOT_SETTERS_ARE_NOT_PUBLIC`
- non-record domain event → `DOMAIN_EVENTS_ARE_RECORDS`
- enumerated transport dependency → `DOMAIN_EVENTS_ARE_TRANSPORT_FREE`
- `id` field raw type not assignable to `ResourceId``NO_LONG_ID_PK`
- project dependency not in registry → `verifyCleanArchitectureDependencies`
- production -> `sample-portfolio` edge → settings registry validation and root dependency verification, plus cross-module ArchUnit rule
## 9. Tests as evidence
### `:domain-core:test`
현재 module에는 executable Java test가 없으므로 이 task의 green result는 domain semantic behavior를 검증한 것이 아니라 **module compile/test task가 현재 build에서 정상 구성되고 완료됨**을 보여준다. 이번 실행 raw evidence는 `evidence/raw/003-domain-core-test.txt`에 보존했다.
### `CleanArchitectureTest`
도메인 purity와 marker-specific rules의 실제 enforcement owner다. 별도 `app-bootstrap` test task로 실행했고 **BUILD SUCCESSFUL / exit code 0**을 확인했다. raw output은 `evidence/raw/004-clean-architecture-test.txt`에 저장했다. 이 테스트는 production class import option을 사용해 `dev.caskeleton` production class graph를 분석한다.
### Sample ID tests
`WorkLogIdPropertyTest`는 UUID **shape** invariant를 property-based로 검증하지만 UUIDv7 version/variant invariant는 검증하지 않는다. `UuidWorkLogIdFactoryTest`는 factory output canonical shape, pairwise distinctness, 1,000회 strict lexical monotonicity를 검증한다. 따라서 “factory가 time-ordered UUIDv7 generator를 사용한다”와 “어떤 ResourceId 입력도 v7만 허용한다”는 서로 다른 claim이다.
## 10. Explicit rationale vs inference
### 문서로 명시된 rationale
- `ResourceId`를 sealed로 만들지 않은 이유: sample module을 production core가 역참조하지 않기 위해서.
- ID generation 책임(contract)과 concrete generation을 분리한 이유: domain purity 유지.
- stereotype annotation을 둔 이유: brittle naming convention 대신 explicit declaration을 ArchUnit 기준으로 사용.
- runtime retention 이유: ArchUnit/reflection reader가 annotation을 볼 수 있게 하기 위해서.
- aggregate public setter rule 한계는 의도적으로 문서화되어 있음.
### 분석 inference
- 현재 domain-core는 구체 business model repository라기보다 skeleton-level domain modeling contract module의 성격이 강하다. 이는 현재 7개 production Java의 실제 내용에서 도출한 해석이다.
## 11. Improvement backlog
### P1 — UUIDv7 계약과 실제 validation의 불일치 확인/정렬
**Fact:** `ResourceId.value()`와 README는 RFC 9562 UUIDv7을 서술하지만 `WorkLogId` regex와 property test는 version/variant를 가리지 않는 모든 canonical UUID-shaped hex 문자열을 허용한다.
**Why it matters:** 외부/rehydration 경로에서 `WorkLogId.of()`로 non-v7 UUID가 들어가도 domain invariant가 거부하지 않는다. 생성 adapter가 v7을 만들기 때문에 정상 create path에서 가려질 수 있다.
**Verification:** `WorkLogId.of("00000000-0000-4000-8000-000000000000")`가 현재 성공하는지 focused test로 고정하고, 계약 의도가 “shape only”인지 “v7 only”인지 결정한다.
**Candidate options:**
1. 계약 문서를 “canonical UUID shape”로 낮춘다.
2. value object가 UUID version 7 + RFC variant를 실제 검증하고 property test를 수정한다.
**Later record candidate:** OPEN QUESTION 또는 DECISION. 의도 확인 전 자동 refactor candidate로 단정하지 않는다.
### P3 — `IdFactory.newId()`의 “never-before-used” 문구 정밀화
**Fact:** interface는 저장소 collision check를 요구하지 않고 sample test도 전역 uniqueness를 증명하지 않는다.
**Why it matters:** API doc을 강한 guarantee로 읽을 가능성이 있다.
**Verification:** identifier 설계 문서/역사에서 uniqueness 의미가 probabilistic UUID uniqueness인지 persistence-level uniqueness인지 확인한다.
**Later record candidate:** REFERENCE 또는 OPEN QUESTION.
## 12. Limitations / exclusions
- sample-portfolio business invariants 전체는 이 scope에서 분석하지 않았다. 위 consumer들은 domain-core contract의 reachability/guardrail 의미를 검증하는 데 필요한 부분만 읽었다.
- `CleanArchitectureTest` 2,792라인 전체의 다른 architecture rules는 해당 future scope에서 분석한다. 여기서는 domain-core contract와 직접 연관된 rule bodies를 읽었다.
- runtime composition closure 전체와 conditional startup wiring은 아직 분석하지 않았다.
- 이 scope의 COMPLETE는 프로젝트 전체 COMPLETE를 의미하지 않는다.
## Source anchors
이 문서가 backtick으로 인용한 타입·경로를 저장소 트리에 대고 해석한 결과다. 해석된 것만 싣는다 — 총 **9개** (main 5 · test 0 · 기타 4).
```
src/domain-core/build.gradle
src/config/architecture/modules.json (domain-core 항목)
main:
src/main/java/dev/caskeleton/domain/identifier/IdFactory.java
src/main/java/dev/caskeleton/domain/identifier/ResourceId.java
src/main/java/dev/caskeleton/domain/stereotype/AggregateRoot.java
src/main/java/dev/caskeleton/domain/stereotype/DomainEvent.java
src/main/java/dev/caskeleton/domain/stereotype/ValueObject.java
기타:
CLAUDE.md
README.md
src/build.gradle
src/settings.gradle
해석되지 않은 인용 (4종) — 외부 타입·문서상 약칭 등:
package-info.java
modules.json
evidence/raw/003-domain-core-test.txt
evidence/raw/004-clean-architecture-test.txt
```