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

17 KiB

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.gradledependencies {}는 비어 있다. 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<?>>

IdFactorynewId() 하나를 가진다. 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다. CleanArchitectureTestset.* 이름의 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.gradleca.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 ResourceIdNO_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