chore: initialize from backend template 0a6dd0e
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# domain-core — pure domain layer
|
||||
|
||||
## Registered identity
|
||||
|
||||
- Module ID: `domain-core`
|
||||
- Gradle path: `:domain-core`
|
||||
- Focused test (derived from Gradle path): `./gradlew :domain-core:test --console=plain`
|
||||
- Runtime baseline: Java 21; repository framework baseline: Spring Boot 4.0.0.
|
||||
- Registry SSOT: `src/config/architecture/modules.json`.
|
||||
|
||||
Package root: `dev.caskeleton.domain`.
|
||||
|
||||
코드 주석에서 덜어낸 **설계 결정의 근거**는 [README.md](README.md) 가 모아둔다 (이 문서는 모듈 규칙 SSOT).
|
||||
|
||||
## Responsibility
|
||||
|
||||
- Business concepts, entities, value objects, enums, domain events, domain services.
|
||||
- Repository ports where this reference implementation still keeps domain-owned persistence contracts.
|
||||
- Domain invariants and core rules.
|
||||
|
||||
## Allowed
|
||||
|
||||
- Java standard library.
|
||||
- Value-only shared operational types only if truly needed.
|
||||
|
||||
## Forbidden
|
||||
|
||||
- Spring annotations.
|
||||
- JPA annotations.
|
||||
- Servlet/HTTP types.
|
||||
- Adapter, application, bootstrap, or presentation DTO dependencies.
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :domain-core:test
|
||||
```
|
||||
@@ -0,0 +1,96 @@
|
||||
# domain-core — 설계 결정 참조
|
||||
|
||||
순수 도메인 계층 모듈. 패키지 루트: `dev.caskeleton.domain`.
|
||||
|
||||
허용/금지 의존, 책임 범위, 테스트 명령 같은 **모듈 규칙**은 [CLAUDE.md](CLAUDE.md) 가 SSOT 다.
|
||||
이 문서는 코드 주석에서 덜어낸 **설계 결정의 근거**를 모아둔 참조용 기록이다 — 코드를 읽다
|
||||
"왜 이렇게 했나"가 궁금할 때 본다. 플랜/스펙 추적용 ID는
|
||||
더 이상 코드 주석에 두지 않고, 그 근거를 아래에 쉬운 말로 풀어 둔다.
|
||||
|
||||
여기서 자주 나오는 "ArchUnit 규칙"이란, 빌드할 때 코드가 정해진 구조를 어겼는지 자동으로
|
||||
검사하는 아키텍처 테스트다. 즉 아래의 "이런 건 금지"라는 결정들은 말로만 정한 약속이 아니라
|
||||
빌드가 실제로 강제한다.
|
||||
|
||||
---
|
||||
|
||||
## identifier — 리소스 식별자
|
||||
|
||||
### `ResourceId` — 모든 식별자가 구현하는 마커 인터페이스
|
||||
|
||||
시스템의 모든 리소스 식별자 값 객체(예: `WorkLogId`)가 구현하는 공통 인터페이스다.
|
||||
식별자의 실제 문자열 값은 `value()` 하나로 노출한다.
|
||||
|
||||
- **왜 `sealed` 로 닫지 않았나.** 원래 레퍼런스 설계는 `permits WorkLogId` 처럼 허용 구현을
|
||||
못 박는 닫힌 집합(`sealed`)으로 만들려 했다. 그런데 `WorkLogId` 같은 실제 식별자는
|
||||
`sample-portfolio`(샘플 코드)에 있고, 이 마커는 재사용 가능한 `domain-core`에 있다.
|
||||
`permits` 절을 쓰는 순간 `domain-core`가 `sample-portfolio`를 의존하게 되는데, 이건
|
||||
모듈 의존성 규칙과 `production_code_does_not_depend_on_sample_portfolio` ArchUnit 규칙이
|
||||
둘 다 금지하는 방향이다. 그래서 `sealed`을 포기했다.
|
||||
- **그럼 "닫힌 집합" 보장은 어디서 하나.** `sealed`이 줬을 보장(아무 타입이나 식별자로
|
||||
쓰지 못하게 하는 것)은 대신 빌드 타임의 `no_long_id_pk` ArchUnit 규칙(D17)이 강제한다 —
|
||||
도메인의 모든 `id` 필드는 반드시 `ResourceId` 구현이어야 하고, 날것의 `Long` PK 는 금지된다.
|
||||
- **`<SELF>` 타입 파라미터(F-bounded generic).** 제네릭이 자기 자신을 다시 가리키는
|
||||
재귀 구조다. 구현 타입이 "나는 나 자신 타입의 `ResourceId`다"라고 선언하게 해서, 자기
|
||||
타입을 그대로 돌려주는 self-typed API 를 타입 안전하게 만들 수 있다.
|
||||
- **`value()` 계약.** 36자 canonical UUID(RFC 9562 UUIDv7) 문자열을 반환한다(D2/D3).
|
||||
|
||||
### `IdFactory` — 식별자 생성 포트
|
||||
|
||||
새 식별자를 발급하는 도메인 포트(인터페이스)다.
|
||||
|
||||
- **"발급할 책임"과 "실제 생성 행위"를 분리한다.** 식별자를 발급할 *책임* 은 도메인(이 포트)이
|
||||
소유한다. 하지만 실제로 식별자를 *만드는 행위* 는 인프라 어댑터(예: UUIDv7 생성기)가
|
||||
수행하고, 애플리케이션 유스케이스가 그 둘을 조율한다. 이렇게 나누면 도메인은 구체적인
|
||||
난수·시계 같은 소스를 전혀 알지 못한 채, 식별자에 대한 계약만 소유한 순수한 상태로 남는다.
|
||||
|
||||
---
|
||||
|
||||
## stereotype — 도메인 모델링 스테레오타입 마커
|
||||
|
||||
`@ValueObject`, `@AggregateRoot`, `@DomainEvent` 세 개의 마커 애너테이션 묶음이다.
|
||||
|
||||
- **왜 이름 규칙이 아니라 마커 애너테이션인가.** 이 애너테이션들은 동작도, 프레임워크 의존도
|
||||
없는 순수 POJO 마커다. 아키텍처 테스트가 "클래스 이름이 `~VO`로 끝나는가" 같은 깨지기 쉬운
|
||||
네이밍 규칙 대신, `@ValueObject` 처럼 **명시적이고 의도가 드러나는 선언**을 기준으로 모델링
|
||||
가드레일을 걸 수 있게 한다.
|
||||
- **왜 프레임워크 중립으로 두나.** 일부러 어떤 프레임워크에도 의존하지 않게 만들어서
|
||||
`domain-core`가 순수 라이브러리로 남게 한다(`domain_is_pure` 가드레일).
|
||||
|
||||
### `@ValueObject` — 값 객체
|
||||
|
||||
불변이고, 개념적 식별자가 없으며, 자기 불변식을 스스로 검증하는 값 타입을 표시한다(D5/D6).
|
||||
|
||||
- **불변식 검증은 유일한 생성 경로에서만.** 값 객체는 record canonical 생성자 또는 팩토리라는
|
||||
단 하나의 생성 경로에서 자기 불변식을 검증한다.
|
||||
- **강제 규칙 — public 무인자 생성자 금지.** `@ValueObject` 타입과 `..domain.vo..` 아래 모든
|
||||
타입은 public 무인자 생성자를 노출하면 안 된다. 빈 생성자는 검증을 건너뛰고 객체를 만들 수
|
||||
있는 "불변식 우회 뒷문"이기 때문이다(`value_objects_have_no_public_no_arg_constructor`).
|
||||
- **왜 `RUNTIME` 리텐션인가.** ArchUnit(바이트코드 검사)과 리플렉션 기반 테스트가 둘 다
|
||||
이 애너테이션을 읽을 수 있어야 해서 런타임까지 유지한다.
|
||||
|
||||
### `@AggregateRoot` — 애그리거트 루트
|
||||
|
||||
애그리거트의 일관성 경계이자, 그 상태를 바꿀 수 있는 유일한 진입점인 타입을 표시한다
|
||||
(D7, Vernon "Effective Aggregate Design").
|
||||
|
||||
- **상태 변경은 의도가 드러나는 메서드로만.** 상태 변경은 불변식을 강제하는, 의도가 분명한
|
||||
애그리거트 메서드를 통해서만 일어나야 한다. 날것의 `setXxx` 세터를 public 으로 열어두면 안 된다.
|
||||
- **강제 규칙 — public 세터 금지.** `@AggregateRoot` 타입의 모든 `set*` 메서드는
|
||||
package-private 또는 protected 여야 한다(`aggregate_root_setters_are_not_public`). 이
|
||||
가시성은 Vernon Option A(ORM 외부에서 매핑하는 방식)로 객체를 재구성할 때 의존하는 지점이다.
|
||||
- **이 규칙의 한계.** ArchUnit 은 정적으로 `set*` 라는 이름 패턴까지만 잡을 수 있다.
|
||||
`applyXxx` 처럼 이름이 다른 상태 변경 메서드는 자동 검사가 닿지 않으므로 코드 리뷰가 잡아야
|
||||
할 몫으로 남는다(spec §4 PRE-DECISION).
|
||||
|
||||
### `@DomainEvent` — 도메인 이벤트
|
||||
|
||||
도메인에서 일어난 사실을 담은, 불변이고 전송수단에 독립적인(transport-free) 타입을 표시한다
|
||||
(D4/D8).
|
||||
|
||||
- **"transport-free"가 무슨 뜻인가.** 이벤트는 도메인 데이터만 담고, 메시지 브로커·와이어
|
||||
포맷·HTTP 같은 전송 계층 타입을 절대 참조하지 않는다는 뜻이다. 도메인 이벤트를 통합 이벤트나
|
||||
와이어 이벤트로 번역하는 일은 애플리케이션/인프라 경계의 책임이지 도메인의 책임이 아니다.
|
||||
- **강제 규칙 두 가지.** 모든 `@DomainEvent` 타입은 (1) `record` 여야 하고
|
||||
(`domain_events_are_records`, 불변성 보장), (2) 어떤 전송 패키지(`org.apache.kafka..`,
|
||||
`org.springframework.http..`, `jakarta.ws.rs..`)도 의존하면 안 된다
|
||||
(`domain_events_are_transport_free`).
|
||||
@@ -0,0 +1,3 @@
|
||||
// Pure domain layer. No Spring, no infra dependencies.
|
||||
dependencies {
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,testAnnotationProcessor
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,testAnnotationProcessor
|
||||
com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs
|
||||
com.github.spotbugs:spotbugs:4.10.2=spotbugs
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,testAnnotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,testAnnotationProcessor
|
||||
com.google.auto:auto-common:1.2.2=annotationProcessor,testAnnotationProcessor
|
||||
com.google.code.findbugs:jsr305:3.0.2=checkstyle,spotbugs
|
||||
com.google.code.gson:gson:2.13.2=spotbugs
|
||||
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.41.0=spotbugs
|
||||
com.google.errorprone:error_prone_annotations:2.47.0=checkstyle
|
||||
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.guava:guava:33.5.0-jre=annotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:guava:33.6.0-jre=checkstyle
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,testAnnotationProcessor
|
||||
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
|
||||
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
|
||||
commons-beanutils:commons-beanutils:1.11.0=checkstyle
|
||||
commons-collections:commons-collections:3.2.2=checkstyle
|
||||
commons-io:commons-io:2.21.0=spotbugs
|
||||
info.picocli:picocli:4.7.7=checkstyle
|
||||
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,testAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,testAnnotationProcessor
|
||||
javax.inject:javax.inject:1=annotationProcessor,testAnnotationProcessor
|
||||
jaxen:jaxen:2.0.0=spotbugs
|
||||
net.bytebuddy:byte-buddy:1.17.8=testCompileClasspath,testRuntimeClasspath
|
||||
net.sf.saxon:Saxon-HE:12.9=checkstyle,spotbugs
|
||||
org.antlr:antlr4-runtime:4.13.2=checkstyle
|
||||
org.apache.bcel:bcel:6.12.0=spotbugs
|
||||
org.apache.commons:commons-lang3:3.20.0=checkstyle,spotbugs
|
||||
org.apache.commons:commons-text:1.15.0=spotbugs
|
||||
org.apache.commons:commons-text:1.3=checkstyle
|
||||
org.apache.httpcomponents:httpclient:4.5.13=checkstyle
|
||||
org.apache.httpcomponents:httpcore:4.4.16=checkstyle
|
||||
org.apache.logging.log4j:log4j-api:2.25.2=spotbugs
|
||||
org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
|
||||
org.apache.maven.doxia:doxia-core:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-logging-api:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-module-xdoc:1.12.0=checkstyle
|
||||
org.apache.maven.doxia:doxia-sink-api:1.12.0=checkstyle
|
||||
org.apache.xbean:xbean-reflect:3.7=checkstyle
|
||||
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
|
||||
org.assertj:assertj-core:3.27.6=testCompileClasspath,testRuntimeClasspath
|
||||
org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle
|
||||
org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
|
||||
org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle
|
||||
org.codehaus.plexus:plexus-utils:3.3.0=checkstyle
|
||||
org.dom4j:dom4j:2.2.0=spotbugs
|
||||
org.javassist:javassist:3.28.0-GA=checkstyle
|
||||
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,testAnnotationProcessor,testCompileClasspath
|
||||
org.junit.jupiter:junit-jupiter-api:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:6.0.1=testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-params:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-commons:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-engine:6.0.1=testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-launcher:6.0.1=testRuntimeClasspath
|
||||
org.junit:junit-bom:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit:junit-bom:6.1.0=spotbugs
|
||||
org.mockito:mockito-core:5.20.0=mockitoAgent
|
||||
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.ow2.asm:asm-analysis:9.10.1=spotbugs
|
||||
org.ow2.asm:asm-commons:9.10.1=spotbugs
|
||||
org.ow2.asm:asm-tree:9.10.1=spotbugs
|
||||
org.ow2.asm:asm-util:9.10.1=spotbugs
|
||||
org.ow2.asm:asm:9.10.1=spotbugs
|
||||
org.pcollections:pcollections:4.0.1=annotationProcessor,testAnnotationProcessor
|
||||
org.reflections:reflections:0.10.2=checkstyle
|
||||
org.slf4j:slf4j-api:2.0.17=spotbugs,spotbugsSlf4j
|
||||
org.slf4j:slf4j-simple:2.0.17=checkstyle,spotbugsSlf4j
|
||||
org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
|
||||
empty=compileClasspath,runtimeClasspath
|
||||
@@ -0,0 +1,15 @@
|
||||
package dev.caskeleton.domain.identifier;
|
||||
|
||||
/**
|
||||
* Domain port for server-assigned resource identifier generation.
|
||||
*
|
||||
* <p>The domain owns the identity contract; the act of generation is performed by an infrastructure
|
||||
* adapter and orchestrated by an application use case. Design rationale in the module README.
|
||||
*
|
||||
* @param <T> the resource identifier type produced by this factory
|
||||
*/
|
||||
public interface IdFactory<T extends ResourceId<?>> {
|
||||
|
||||
/** Mints a fresh, never-before-used identifier. */
|
||||
T newId();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package dev.caskeleton.domain.identifier;
|
||||
|
||||
/**
|
||||
* Marker for every resource identifier value object in the system.
|
||||
*
|
||||
* <p>Deliberately <strong>not</strong> {@code sealed} (the closed set is enforced by the {@code
|
||||
* no_long_id_pk} ArchUnit rule instead). Design rationale in the module README.
|
||||
*
|
||||
* @param <SELF> the implementing identifier type (F-bounded for self-typed APIs)
|
||||
*/
|
||||
public interface ResourceId<SELF extends ResourceId<SELF>> {
|
||||
|
||||
/** The canonical 36-character UUID string (RFC 9562 UUIDv7). */
|
||||
String value();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Domain layer anchor for project-owned business concepts. */
|
||||
package dev.caskeleton.domain;
|
||||
@@ -0,0 +1,19 @@
|
||||
package dev.caskeleton.domain.stereotype;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Marks a type as an <strong>aggregate root</strong> — the single consistency boundary and the only
|
||||
* entry point through which the aggregate's state may change.
|
||||
*
|
||||
* <p>Pure marker, {@code RUNTIME} retention. Design rationale (Vernon "Effective Aggregate Design",
|
||||
* enforced guardrails, the {@code set*} static-analysis limit) in the module README.
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AggregateRoot {}
|
||||
@@ -0,0 +1,19 @@
|
||||
package dev.caskeleton.domain.stereotype;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Marks a type as a <strong>domain event</strong> — an immutable, transport-free fact about
|
||||
* something that happened in the domain.
|
||||
*
|
||||
* <p>Pure marker, {@code RUNTIME} retention. Design rationale (transport-free meaning, enforced
|
||||
* guardrails) in the module README.
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface DomainEvent {}
|
||||
@@ -0,0 +1,19 @@
|
||||
package dev.caskeleton.domain.stereotype;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Marks a type as a domain <strong>value object</strong> — immutable, no conceptual identity,
|
||||
* self-validating on its sole construction path (canonical record constructor or factory).
|
||||
*
|
||||
* <p>Pure marker, {@code RUNTIME} retention. Design rationale (enforced guardrails) in the module
|
||||
* README.
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValueObject {}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Domain modeling stereotype markers — pure, framework-neutral POJO markers that let the
|
||||
* architecture suite key modeling guardrails off an explicit declaration rather than a brittle
|
||||
* naming convention. Design rationale in the module README.
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link dev.caskeleton.domain.stereotype.ValueObject} — immutable, self-validating value
|
||||
* type.
|
||||
* <li>{@link dev.caskeleton.domain.stereotype.AggregateRoot} — consistency boundary whose state
|
||||
* mutates only through its own methods.
|
||||
* <li>{@link dev.caskeleton.domain.stereotype.DomainEvent} — transport-free fact emitted by the
|
||||
* domain.
|
||||
* </ul>
|
||||
*/
|
||||
package dev.caskeleton.domain.stereotype;
|
||||
Reference in New Issue
Block a user