Files
llm-wiki/raw/branch-notes/feature-domain-modeling-guardrails.md
T

40 KiB
Raw Blame History

title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, governing_docs, related_projects, tags, created, target_merge, status_label, contract_packet_sha256
title source_type status id kind project work_item inherits refines overrides depends_on contract_packet branch parent_branch governing_docs related_projects tags created target_merge status_label contract_packet_sha256
branch / feature-domain-modeling-guardrails branch-note raw BR-CA-SKELETON-OPERATIONAL-CONTRACT-036 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-036
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1
1 feature-domain-modeling-guardrails
wiki/projects/ca-tmpl/privacy-file-domain-modeling
wiki/projects/ca-tmpl/clean-architecture-package-layout
ca-skeleton
branch
ca-skeleton
domain
modeling
guardrails
2026-05-22 in-progress 12147734b0020a89b2ffd64b9840c0a563c7a44fa50b2c121596005623139fe7

branch: feature-domain-modeling-guardrails

Layer: raw/branch-notes/ — domain layer가 framework와 persistence에 오염되지 않도록 modeling guardrail을 정의합니다.

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: domain model forbidden dependency fixture가 실패한다

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1 Gradle multi-module에서 domain-core·application-core·adapter-*·shared-contract·app-bootstrap·sample-portfolio 책임을 분리한다 domain-core의 framework·persistence 의존 금지 경계에 적용 raw/project-notes/ca-skeleton-operational-contract

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

목표

도메인을 바로 얹을 수 있는 skeleton이 되려면 domain layer가 깨끗해야 합니다. entity, value object, domain service, domain event의 역할을 구분하고, framework annotation이나 persistence model이 domain으로 들어오는 것을 막습니다.

  • 이슈:
  • PR:

범위

포함 범위

  • entity/value object/domain service/domain event 구분.
  • domain invariant 위치.
  • domain forbidden dependency.
  • aggregate state mutation 기준.
  • domain exception 범위.

제외 범위

  • DDD 전술 패턴 전체 강제.
  • 특정 aggregate 설계.
  • business naming convention.

TODO

TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Decisionized Work Items" / "테스트 계약" 참조. entity/value object/domain service, invariant 위치, aggregate mutation, forbidden dependency, domain exception, domain event modeling 모두 표 row 또는 결정 라인으로 반영됨. 잔존 TODO 없음.

Work Item Contract

각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 기준 작성으로 남아 있으면 이 branch는 완료로 보지 않습니다.

field required rule
Decision yes 구현자가 선택해야 하는 기본값
Allowed yes 허용되는 예외와 조건
Forbidden yes 절대 금지되는 구현/문서 상태
Required registry update conditional error/env/header/log/metric/capability 변경 시 필수
Required contract test yes 계약 위반 시 실패해야 하는 테스트
Failure condition yes review/build에서 실패로 판정할 상태
Canonical extraction target yes wiki/projects 승급 위치

진행 중 메모

  • 2026-06-05 ground-truth 대조 (/branch-spec): ca-tmpl domain_is_pure ArchUnit rule (CleanArchitectureTest.java:36-57) 이 ..domain.. 의 Spring/JPA/Hibernate/Lombok/cross-layer import 를 금지 — owner 는 raw/branch-notes/feature-architecture-enforcement-rules D3 (rule 의 .as(...) 주석에 명시). 본 branch 의 D1(framework-neutral) 은 이 rule 을 재정의하지 않고 위임/재사용 한다 (자세한 정합/drift 는 §Audit & Findings).
  • 본 branch 의 modeling-specific guardrail (VO constructor / aggregate mutator 가시성 / logger ban / domain event) 은 모두 코드 미존재 = planned. @ValueObject·@AggregateRoot·@DomainEvent annotation 도 src/ grep 결과 미존재. domain-core 모듈에는 현재 identifier/ResourceId·IdFactory 만 존재.
  • 2026-06-05 C2 구현 완료 (locally-verified): 위 5개 modeling-specific guardrail 을 전부 구현. 자세한 구현 facts/검증/상태 전이는 §구현 기록 (2026-06-05) 참조. §진행 중 메모의 "코드 미존재" 서술은 2026-06-05 이전 ground-truth 기준이며, 현재는 §구현 기록이 최신 상태를 가진다.

구현 기록 (2026-06-05)

Phase C2 실 코드 작성. ca-tmpl repo feature-domain-modeling-guardrails branch. 증거 등급: 아래 모두 locally-verified (focused gradle test + verifyCleanArchitectureDependencies 통과).

변경 파일

  • domain-core (신규 marker 패키지 dev.caskeleton.domain.stereotype):
    • ValueObject.java, AggregateRoot.java, DomainEvent.java@Target(TYPE), @Retention(RUNTIME), java.lang.annotation 만 의존 (framework-neutral 유지, domain_is_pure 통과).
    • package-info.java — marker 의도 문서화.
  • app-bootstrap CleanArchitectureTest.java (신규 규칙 5종 + custom condition 2종):
    • domain_has_no_logger (D3) — ..domain..org.slf4j../java.util.logging../ch.qos.logback../org.apache.logging.log4j.. import 금지. domain_is_pure별도 규칙(F1 owner 경계 보존).
    • value_objects_have_no_public_no_arg_constructor (D5/D6) — @ValueObject OR ..domain.vo.. → public no-arg 생성자 부재. custom notHaveAPublicNoArgConstructor().
    • aggregate_root_setters_are_not_public (D7) — @AggregateRootset.* method notBePublic().
    • domain_events_are_records (D4/D8) — @DomainEvent 는 record. custom beRecordTypes() (JavaClass.isRecord()).
    • domain_events_are_transport_free (D4/D8) — @DomainEventorg.apache.kafka../org.springframework.http../jakarta.ws.rs.. 의존 금지.
  • app-bootstrap violation fixtures (비공허 증명, violations-as-data): violations/domain/LoggerUsingDomainFixture, AnnotatedPublicNoArgValueObjectFixture, vo/PackagePublicNoArgValueObjectFixture, PublicSetterAggregateFixture, event/{kafka,springhttp,jaxrs,nonrecord}/*Fixture + ArchitectureViolationFixtureTest 에 11개 assertion(글로브별 격리 + over-block guard 2종).
  • app-bootstrap build.gradle: testCompileOnly kafka-clients, jakarta.ws.rs-api (transport glob 격리 증명용, test scope).
  • sample-portfolio (positive coverage + Claims To Verify PoC):
    • WorkLog @AggregateRoot + blank-title 불변식(requireValidTitleWorkLogInvariantException).
    • Period, WorkLogId @ValueObject.
    • WorkLogInvariantException(+ safe Reason enum) — 도메인은 operational error code 모름(D2), logger 안 씀(D3).
    • WorkLogReserved(@DomainEvent record, transport-free) → application/event/WorkLogReservedIntegrationEvent + ...Mapper (경계 변환 PoC).
    • 테스트: WorkLogInvariantTest, WorkLogIdPropertyTest(jqwik property-based), WorkLogReservedIntegrationEventMapperTest. build.gradletestImplementation net.jqwik:jqwik:1.9.1.

검증 명령 / 결과

  • cd src && ./gradlew :domain-core:test :sample-portfolio:test :app-bootstrap:test verifyCleanArchitectureDependenciesBUILD SUCCESSFUL.
  • ArchitectureViolationFixtureTest → tests=40, failures=0, skipped=0 (신규 11개 포함).
  • WorkLogIdPropertyTest → jqwik property 3종 통과.
  • ca-architect-sentinel 작업트리 감사 → PASS (FAIL/WARN 0; domain framework-neutral 유지, application.event 의 domain→application 방향만 의존, 불변식이 aggregate 안에 위치).

함정

  • @DomainEvent record 의 component 로 testCompileOnly transport type 을 두자 JUnit test discovery 가 통째로 실패(ClassSelector resolution failed). record component = canonical ctor 시그니처라 reflective discovery 가 즉시 resolve. method body .class 참조 + subpackage importPackages 로 회피. → raw/errors/archunit-testcompileonly-class-loading-2026-06-02 2026-06-05 addendum.

상태 전이 (planned → locally-verified)

  • D3 logger ban, D5/D6 VO 불변식, D7 aggregate mutator, D4/D8 domain event(record + transport-free): plannedlocally-verified.
  • §Claims To Verify 의 VO property / aggregate set* / logger import / transport-free mapping 항목: plannedlocally-verified (PoC 코드 + 테스트 존재).
  • Greg Young/Vernon paraphrased 근거 검증(외부 원전 대조)은 여전히 needs-confirmation — 코드 구현과 무관하게 미해결.

결정 사항

  • 2026-05-22: domain은 Spring/JPA/HTTP/Security/Logging type을 알지 않음.
  • 2026-05-22: domain exception은 business invariant만 표현하고 operational error code를 직접 알지 않음.
  • 2026-05-22: domain logger는 금지. invariant 위반 사유는 domain exception의 safe reason enum/value로 표현하고 application layer가 로그로 번역.
  • 2026-05-22: domain event는 transport-free fact만 표현하고 integration event mapping은 application/infrastructure 경계에서 수행.

판정 기준

구분 기준
Decision domain model은 framework-neutral pure model로 유지
Allowed domain event/value object 내부의 순수 validation
Forbidden @Entity, @Service, HTTP/JPA/Security/Logger import
Required checks forbidden import, public mutable state, domain-to-response direct exposure
Failure condition domain이 infrastructure/presentation/application response type을 알면 실패

Decisionized Work Items

item Decision Allowed Forbidden Required test
entity/value object pure domain types only immutable helper libraries JPA entity as domain forbidden import test
invariant value object/entity constructor/factory application pre-check for UX DB-only invariant invalid state test
mutation aggregate method controls state package-private constructor for ORM outside domain model public mutable fields mutation test
diagnostics safe reason enum, application logs no reason for security-sensitive cases domain logger logger import test
domain event transport-free fact internal-only event Kafka/HTTP/Slack detail event model test

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. Decision ID 는 본 branch-note 안에서 안정적으로 유지 (D1~D8). Decisionized Work Items 표 row 와 1:1 매핑.

Decision ID Decision Supporting Claims Evidence Strength Open Risk
D1 domain 은 Spring/JPA/HTTP/Security/Logging type 을 알지 않음 (framework-neutral) raw/official-docs/domain-fowler-anemic-vs-rich-model.md#FOWLER-ANEMIC-C1, raw/official-docs/domain-fowler-anemic-vs-rich-model.md#FOWLER-ANEMIC-C5, raw/company-tech-blogs/domain-woowahan-ddd-aggregate-techblog.md#WOOWA-HEX-C2 engineering-blog + company-case-study Fowler bliki 는 engineering-blog 등급 (개인 블로그, official-vendor-doc 격상 금지). Logger ban 의 직접 출처 부재 — FOWLER-ANEMIC-C5 "validations/calculations/business rules" 에서 도출 가능하나 약함
D2 domain exception 은 business invariant 만 표현, operational error code 를 직접 알지 않음 raw/official-docs/domain-fowler-anemic-vs-rich-model.md#FOWLER-ANEMIC-C5, raw/official-docs/domain-vaughn-vernon-aggregate-root.md#VERNON-AGG-C2 engineering-blog + needs-confirmation VERNON-AGG-C2 는 paraphrased (needs-confirmation) — PDF 본문 verbatim 미확보. "operational error code 와 domain exception 의 분리" 직접 출처 부재
D3 domain logger 금지, invariant 위반 사유는 safe reason enum/value 로 표현 후 application layer 가 로그로 번역 UNSUPPORTED_DECISION (Fowler/Vernon 모두 logger ban 명시 부재 — FOWLER-ANEMIC-C5 의 "domain logic = validations/calculations/business rules" 에서 logger 부재가 도출되나 직접 인용 아님) Logger ban 의 공식 표준 출처 없음 — ca-tmpl 자체 결정. "safe reason enum" 패턴의 reference 부재
D4 domain event 는 transport-free fact 만 표현, integration event mapping 은 application/infrastructure 경계에서 수행 raw/company-tech-blogs/domain-event-sourcing-vs-cqrs-greg-young.md#GY-CQRS-C4, raw/official-docs/domain-vaughn-vernon-aggregate-root.md#VERNON-AGG-C5 needs-confirmation + needs-confirmation (paraphrased) GY-CQRS-C4 는 needs-confirmation (Greg Young PDF 검증 실패, Confluent corroborate 만). VERNON-AGG-C5 도 paraphrased — transport-free 의 ca-tmpl 정의는 자체 차용
D5 entity / value object 는 pure domain types only, JPA entity 를 domain 으로 두지 않음 (Vernon Option A) raw/official-docs/domain-vaughn-vernon-aggregate-root.md#VERNON-AGG-C6, raw/official-docs/domain-fowler-anemic-vs-rich-model.md#FOWLER-ANEMIC-C3, raw/company-tech-blogs/domain-woowahan-ddd-aggregate-techblog.md#WOOWA-HEX-C2 needs-confirmation + engineering-blog + company-case-study VERNON-AGG-C6 paraphrased (needs-confirmation). 우아한형제들 사례는 Option A (POJO domain) 와 Option B (JPA in domain) 모두 보이는 vendor-specific — Vernon 의 Option A/B 분리 자체는 본 Claim 으로 직접 증명 안 됨
D6 invariant 는 value object/entity constructor/factory 에 위치, DB-only invariant 금지 raw/official-docs/domain-vaughn-vernon-aggregate-root.md#VERNON-AGG-C2, raw/official-docs/domain-fowler-anemic-vs-rich-model.md#FOWLER-ANEMIC-C5 needs-confirmation + engineering-blog VERNON-AGG-C2 paraphrased — "single transaction" 의 의미가 "constructor invariant" 와 정확히 매핑되는지 PDF verbatim 확인 필요
D7 aggregate mutation 은 root method 만 controls, public mutable field 금지, ORM 외부 매핑 (Option A) 으로 package-private constructor 사용 raw/official-docs/domain-vaughn-vernon-aggregate-root.md#VERNON-AGG-C6 needs-confirmation (paraphrased — IDDD Ch.10 도서 인용, 페이지/문단 미지정) VERNON-AGG-C6 verbatim 미확보. "package-private/protected" 가 Java 외 다른 JVM 언어 (Kotlin internal) 에 매핑되는지 별도 검증 필요
D8 domain event modeling 은 internal-only event 허용, Kafka/HTTP/Slack detail 금지 raw/company-tech-blogs/domain-event-sourcing-vs-cqrs-greg-young.md#GY-CQRS-C4, raw/company-tech-blogs/domain-event-sourcing-vs-cqrs-greg-young.md#GY-CQRS-C3 needs-confirmation (Greg Young PDF 미검증) "transport-free" 의 ca-tmpl 정의는 차용 (GY-CQRS-C4 Does not prove: transport-free 가능성 명시 부재) — 직접 출처 없음

구현 가이드

결정 이 "무엇" 이라면 본 §는 "어디에 어떻게". 본 branch 의 modeling guardrail 은 전부 planned (코드 미존재) 이므로, 아래는 C2 진입 시 되묻지 않고 작성할 수 있는 사전 명세다. anchor 는 §진행 중 메모 / §Audit 에서 확인한 실제 ca-tmpl 구조(domain_is_pure, domain-core 모듈, feature-architecture-enforcement-rules owner)에 정합시킨다. 3-rule (CLAUDE.md §15.5): 각 cell 은 Decision ID + Supporting Claim ID reference (R1) / 근거 없는 detail 은 UNSUPPORTED_IMPL_DECISION + trade-off 한 줄 (R2) / 범위 밖은 §Audit 으로 이관 (R3).

1. 도메인 순수성 — 기존 rule 위임 (재정의 금지)

Trace: D1 ↔ FOWLER-ANEMIC-C1/C5, WOOWA-HEX-C2. 단, 정적 강제의 owner 는 본 branch 가 아님.

  • OUT_OF_BRANCH_SCOPE (위임): framework-neutral 정적 강제(..domain.. 의 Spring/JPA/Hibernate/Lombok import 금지)는 raw/branch-notes/feature-architecture-enforcement-rules D3 의 domain_is_pure (CleanArchitectureTest.java:36-57, actually-implemented) 가 소유. 본 branch 는 이 rule 을 재정의/복제하지 않고 모델링 결정의 전제로 위임 참조. 본 branch 가 추가하는 것은 아래 2~5 의 modeling-specific rule 뿐.
항목 owner 상태 anchor
..domain.. Spring/JPA/Hibernate/Lombok/cross-layer import 금지 raw/branch-notes/feature-architecture-enforcement-rules D3 actually-implemented domain_is_pure (CleanArchitectureTest.java:36)
controller 가 domain/entity 타입 직접 반환 금지 raw/branch-notes/feature-boundary-validation-mapping-contract D8 actually-implemented controllers_do_not_return_domain_or_entity_types (CleanArchitectureTest.java:321)

Option A vs B 선택 근거는 추측이 아니라 코드로 증명된다 (D5·D7 강화): Vernon Option B(domain class 에 @Entity/JPA annotation 직접 부착)는 domain 패키지에 jakarta.persistence.. import 를 유발한다. 이는 domain_is_pure 의 forbidden list (CleanArchitectureTest.java:40-42jakarta.persistence..·javax.persistence..) 에서 자동 위반되어 빌드가 깨진다 (actually-implemented). 따라서 ca-tmpl 에서 Option A(ORM 외부 매핑)는 선호가 아니라 기존 정적 강제의 논리적 귀결 — Option B 는 코드 레벨에서 이미 금지됨. 이 체인이 VERNON-AGG-C6 의 paraphrased 약점(도서 페이지 미확보)을 코드 ground-truth(L2)로 보완한다.

2. 도메인 logger ban 정적 강제 (D3)

Trace: D3 (UNSUPPORTED_DECISION — logger ban 의 공식 출처 없음, ca-tmpl 자체 결정).

  • GAP / STALE_OWNER 위험: 코드 확인 결과 domain_is_pure 의 forbidden 목록에 logging framework 가 없다 (org.slf4j·java.util.logging·ch.qos.logback·org.apache.logging.log4j 모두 미포함; test 파일 전체 grep 상 logger ban rule 부재). 따라서 "domain 이 Logger import 시 ArchUnit 실패" 는 현재 planned 이며 어떤 rule 도 강제하지 않음.
  • 근거 등급 확정 (되묻기 방지): logger ban 의 공식 표준 출처는 존재하지 않는다 — 이는 clean-architecture 통념이지 official standard 가 아니다 (D3 UNSUPPORTED_DECISION 유지). 구현자는 "공식 근거를 더 찾아라"가 아니라 ca-tmpl 자체 규약으로 확정하고 착수한다. 사실 등급은 격상하지 않으며, 외부(면접/README)에서 "표준이라 막았다"고 말하지 않는다.
  • PRE-DECISION (메커니즘 확정): 별도 rule domain_has_no_logger 신설 (owner = 본 branch). domain_is_pure forbidden list 확장(대안)을 택하지 않는 이유는 코드 근거가 있다 — domain_is_pure 의 owner 는 raw/branch-notes/feature-architecture-enforcement-rules D3 (CleanArchitectureTest.java:54-56 .as(...) 명시, §Audit F1). 그 list 에 logger 를 끼우면 본 branch 의 결정이 타 branch owner rule 에 섞여 owner 경계가 깨진다(F1 회피). 별도 rule 은 위반 메시지도 "domain logger 금지(D3)"로 명확. → trade-off 가 아니라 owner-boundary 로 강제됨.
강제 대상 메커니즘(제안) 상태
..domain..org.slf4j..·java.util.logging..·ch.qos.logback..·org.apache.logging.log4j.. import 신규 rule domain_has_no_logger (owner = 본 branch; forbidden list 확장 아님 — F1 owner 경계 보존) planned
invariant 위반 사유 = safe reason enum/value (noun 형태), 로그 번역은 application layer domain exception 의 reason enum 필드 + application 에서 error.category 매핑 planned

3. Value Object invariant 강제 (D5·D6)

Trace: D5 ↔ VERNON-AGG-C6·FOWLER-ANEMIC-C3·WOOWA-HEX-C2, D6 ↔ VERNON-AGG-C2·FOWLER-ANEMIC-C5.

  • PRE-DECISION (탐지 기준·명명 확정): annotation @ValueObjectprimary marker, ..domain.vo.. package convention 을 fallback(annotation 미부착 VO 도 포착)으로 둘 다 사용 — ArchUnit rule 의 .areAnnotatedWith(...).or().resideInAPackage(...) 가 양쪽을 OR 로 묶으므로 둘 중 택일이 아니라 합집합이 자연스럽다. annotation 패키지는 dev.caskeleton.domain.stereotype (domain-core 신규 marker 패키지; 현재 domain-core 는 identifier 패키지만 보유 → marker 패키지 신설). 근거 raw(Vernon/Fowler)는 invariant 위치만 권고하고 명명은 권고 안 하므로 @ValueObject·stereotype 명칭은 ca-tmpl 임의 — 사실 등급 비격상, 코드 미존재이므로 planned.
강제 대상 메커니즘(제안) 상태
@ValueObject 또는 ..domain.vo.. 의 record/class 에 public no-arg constructor 부재 classes().that().areAnnotatedWith(ValueObject.class).or().resideInAPackage("..domain.vo..").should().notHaveAccessibleNoArgConstructor() planned
모든 VO constructor 가 invalid input 에 domain exception/IllegalArgumentException throw property-based test (jqwik) — null/empty/boundary × N planned
@ValueObject annotation 신설 dev.caskeleton.domain.stereotype.ValueObject (domain-core 신규 marker 패키지) planned (annotation 미존재)

4. Aggregate root mutator 가시성 (D7)

Trace: D7 ↔ VERNON-AGG-C6 (needs-confirmation — IDDD Ch.10 페이지 미지정).

  • PRE-DECISION (탐지 범위 확정): ArchUnit 정적 강제 범위 = set.* prefix method 만 (notBePublic()). 이유: ca-tmpl 은 현재 Java-only (src/ 전부 .java) 이므로 Kotlin internal/copy()·record wither 우회는 지금 범위 밖(D7 Open Risk 로 보존, Kotlin 도입 시 재검토). set.* 외의 state-changing method(예: applyXxx, markAsXxx)는 ArchUnit 로 일반 강제가 불가능 → 코드리뷰 + 네이밍 컨벤션으로 보완(정적 강제 아님 명시). annotation 패키지는 §3 과 동일하게 dev.caskeleton.domain.stereotype.AggregateRoot. @AggregateRoot 명명 ca-tmpl 임의(코드 미존재, planned).
강제 대상 메커니즘(제안) 상태
@AggregateRoot class 의 set*/state-changing method 가 public 아님(package-private/protected) methods().that().haveNameMatching("set.*").and().areDeclaredInClassesThat().areAnnotatedWith(AggregateRoot.class).should().notBePublic() planned
ORM 재구성용 constructor 가시성 = package-private (Vernon Option A, ORM 외부 매핑) persistence mapper 가 domain 밖에서 재구성 (WorkLogWorkLogJpaEntity) planned
@AggregateRoot annotation 신설 dev.caskeleton.domain.stereotype.AggregateRoot planned (annotation 미존재)

5. Domain event transport-free 모델링 (D4·D8)

Trace: D4 ↔ GY-CQRS-C4·VERNON-AGG-C5 (둘 다 needs-confirmation), D8 ↔ GY-CQRS-C3/C4.

  • 근거 등급 확정 (되묻기 방지): "transport-free fact" 라는 명칭/정의는 ca-tmpl 차용이며 Greg Young 원전이 직접 보장하지 않는다(GY-CQRS-C4 needs-confirmation, D8 Open Risk). 구현자는 이 명칭의 출처를 더 추적하지 않는다 — 보수적 기본값으로 확정 후 착수. 사실 등급 비격상.
  • PRE-DECISION (경계 확정, 코드로 부분 강제됨): domain event 는 ..domain.. 의 immutable record 로 두고 integration event 변환은 application/adapter 경계의 mapper 책임. 이 경계는 추측이 아니라 부분적으로 코드로 강제된다domain_is_pure..domain....adapter.. import 를 금지(CleanArchitectureTest.java:47)하므로, domain event 가 adapter 의 integration-event/transport 타입을 참조하면 자동 위반(actually-implemented). 단, Kafka/HTTP 클라이언트 SDK 패키지(org.apache.kafka.. 등)는 현재 forbidden list 에 없으므로 그 한 가지는 본 branch 의 domain_has_no_logger 와 같은 추가 rule 또는 코드리뷰로 보완 (planned).
강제 대상 메커니즘(제안) 상태
domain event = immutable record, transport(Kafka/HTTP/Slack) 필드 부재 @DomainEvent record + ArchUnit forbidden import — 금지 패키지: org.apache.kafka..(Kafka SDK), org.springframework.http../jakarta.ws.rs..(HTTP), 슬랙 등 outbound client SDK. UNSUPPORTED_IMPL_DECISION: broker/transport 추가 시 목록 갱신 필요(현재 ca-tmpl 미사용 SDK 는 미열거) planned
integration event 변환은 application/infrastructure 경계 application mapper: WorkLogReserved(domain) → WorkLogReservedIntegrationEvent(application) → publish(infra) planned

엣지·실패·의존

R4(깊이 게이트) 캡처용. 본 branch 의 modeling guardrail 이 구현 중 부딪힐 실패/엣지/계약 의존.

  • 실패·엣지 경로:
    • ORM 재구성이 invariant 를 우회 — package-private/no-arg constructor 를 ORM(Hibernate) 이 reflection 으로 호출해 객체를 만들 때 constructor invariant 가 호출되지 않을 수 있음. 기대 동작: ORM 재구성은 이미 valid 한 영속 상태에서만 일어난다는 전제 + 매핑은 domain 밖 mapper 책임(Vernon Option A). VO no-arg constructor 금지 rule 과 ORM 요구의 충돌은 "ORM 외부 매핑"으로 회피.
    • Kotlin data class copy() 우회 — copy() 가 constructor invariant 를 호출하지 않으면 invalid VO 생성 가능. 기대 동작: D7 Open Risk 로 이미 기록 — JVM 언어별 검증 필요.
    • safe reason enum 의 정보 노출 — security-sensitive invariant 위반 사유를 enum 으로 노출하면 client 에 단서 제공 가능. 기대 동작(Decisionized Work Items): security-sensitive case 는 reason 제공 안 함, application 이 일반화된 error.category 로만 번역.
  • 다른 계약 의존:
    • raw/branch-notes/feature-architecture-enforcement-rulesdomain_is_pure (D3) 에 의존 — domain framework-neutrality 의 정적 강제 owner. 이 rule 의 forbidden list/package 패턴이 바뀌면 본 branch 의 D1 전제가 흔들린다.
    • operational error code SSOT = feature-operational-error-observability-foundation + docs/registries/error-codes.yaml. safe reason enum → error.category 번역은 그 계약을 consume (domain 은 operational code 를 직접 알지 않음 = D2).
    • persistence 매핑(Vernon Option A) → feature-boundary-validation-mapping-contract / persistence adapter 의 mapper 계약에 의존.

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트의 실 동작을 자동으로 보장하지 않음.

Claim Why uncertain How to verify Status
..domain.vo.. package 의 모든 record/class 가 public no-arg constructor 없이 invariant 강제 가능 VERNON-AGG-C2 paraphrased — VO 의 constructor invariant 가 모든 valid input 에서 작동하는지 property-based test 필요 sample feature 의 VO 1개에 jqwik property-based test 적용 → null/empty/invalid input × N 종 자동 생성 → exception 확인 planned
@AggregateRoot annotated class 의 모든 set* method 가 package-private/protected 이며 invariant 호출 포함 VERNON-AGG-C6 paraphrased — ORM-friendly constructor 가시성의 verbatim 미확보 ArchUnit methodsThat().haveName("set.*").andAreDeclaredInClassesThat().areAnnotatedWith(@AggregateRoot.class).should().notBePublic() 작성 + 위반 케이스 테스트 planned
domain class 가 Logger import 시 ArchUnit 이 실패시킨다 D3 UNSUPPORTED — logger ban 의 공식 출처 부재 ArchUnit forbidden import test 작성 (slf4j, logback, log4j 모두 포함) → sample domain 에 임시 logger 추가 시 실패 케이스 capture planned
Vernon Option A (domain ↔ JpaEntity 외부 매핑) 가 Option B (domain 에 JPA annotation) 보다 ca-tmpl 의 forbidden import 규칙과 더 정합 VERNON-AGG-C6 paraphrased + 우아한형제들 WOOWA-HEX-C2 (Option A) + Option B reference 부재 sample-portfolio 에 WorkLog(domain) ↔ WorkLogJpaEntity(infrastructure) 분리 PoC + MapStruct 매핑 → ArchUnit forbidden import test 통과 확인 needs-confirmation
domain event 가 transport-free 로 정의되어도 application/infrastructure 경계에서 integration event 변환 가능 D4 paraphrased only — Vernon eventual consistency / Greg Young event immutability 만 근거, transport mapping 패턴 직접 출처 부재 sample feature 에 WorkLogReserved (domain event) → WorkLogReservedIntegrationEvent (application mapper) → Kafka publish (infrastructure) 흐름 PoC planned
한국 백엔드 현장에서 Spring 기본 튜토리얼이 anemic default 라는 메모가 ca-tmpl 강제 결정의 정당화에 충분 FOWLER-ANEMIC-C2 의 일반 명제만 있고 "한국 현장 관찰" 의 별도 출처 없음 (메모) 별도 raw 자료 (Inflearn / 김영한 강의 / 우아한형제들 hands-on) 의 default 패턴 추출 후 ingest planned
Greg Young / Vernon 의 paraphrased claim 들이 PDF / IDDD 원전과 일치 GY-CQRS-C1C4, VERNON-AGG-C2C6 모두 needs-confirmation (a) Greg Young CQRS PDF 재페치 시도 (대안: archive.org / Fowler bliki cross-check) (b) IDDD Ch.10 도서 인용 페이지/문단 명시 추가 needs-confirmation

테스트 계약

  • domain package가 Spring/JPA/HTTP/security/logging package를 import하면 실패.
  • VO invalid state 검사: 모든 @ValueObject annotation이 붙은 class 또는 features.*.domain.vo. package의 record/class는 다음을 만족: (a) public no-arg constructor 없음 (b) 모든 constructor에서 invariant violation 시 IllegalArgumentException 또는 domain exception throw. 측정 방법: ArchUnit classes().that().areAnnotatedWith(@ValueObject.class).or().resideInAPackage("..domain.vo..").should().notHaveAccessibleNoArgConstructor() + property-based test on each VO with null/empty/invalid input → exception expected.
  • aggregate mutation 검사: @AggregateRoot annotation이 붙은 class의 모든 mutator method (set* prefix 또는 state-changing method)는 (a) public이 아닌 package-private 또는 protected이고 (b) invariant 검증 로직 포함. 측정 방법: ArchUnit methodsThat().haveName("set.*").andAreDeclaredInClassesThat().areAnnotatedWith(@AggregateRoot.class).should().notBePublic(). setter가 public이거나 invariant 호출 없이 state 변경 시 fail.
  • domain package가 Logger 또는 operational error code를 직접 알면 실패. (rule: domain_has_no_logger, D3 — owner = 본 branch. §구현 가이드 §2 참조. domain_is_pure 와 별개 rule)

근거 (필수, 최소 1개+)

본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.

Source 정당화하는 결정
raw/official-docs/domain-vaughn-vernon-aggregate-root Vernon "Effective Aggregate Design" 4 rules + small aggregate + ORM-friendly constructor (ca-tmpl Option A 채택
raw/official-docs/domain-fowler-anemic-vs-rich-model Fowler "Anemic Domain Model" anti-pattern (rich model 강제의 reference
raw/company-tech-blogs/domain-woowahan-ddd-aggregate-techblog 우아한형제들 초기 글 사례; ca-tmpl forbidden import 규칙 위배라 거부
raw/company-tech-blogs/domain-event-sourcing-vs-cqrs-greg-young Greg Young; ca-tmpl 미채택, "transport-free fact" 정의만 차용
raw/official-docs/cqrs-fowler-bliki CQRS command/query 모델 분리 정의 + Fowler 의 "very cautious" 보수적 권고 (Fowler martinfowler.com bliki — engineering-blog 등급, official-standard 아님). ca-tmpl 의 command/query use case 분리 (Out of scope: read/write 데이터 모델 분리) 의 대비 reference. ca-tmpl 은 CQRS-FOWLER-C3 (개념 모델 분리) 만 차용, CQRS-FOWLER-C5/C6 (cautious + complexity) 에 따라 read/write 저장소 분리는 미채택

외부 근거 / 대안 조사 (2026-05-22 — Group G-J: Domain Modeling Guardrails)

본 branch의 VO with private constructor + aggregate root mutator package-private/protected + domain logger ban + safe reason enum + invariant in constructor + ORM 외부 매핑 결정에 대한 외부 source.

  • 채택 결정 (Rich domain model + Vernon Aggregate Root Option A: ORM 외부 매핑):
  • 검토한 대안:
    • 대안 1: Anemic domain modeldomain-fowler-anemic-vs-rich-model 동일 source에서 anti-pattern으로 정의 (ca-tmpl 거부)
    • 대안 2: Vernon Option B (JPA direct annotation in domain)raw/company-tech-blogs/domain-woowahan-ddd-aggregate-techblog (우아한형제들 초기 글 사례; ca-tmpl forbidden import 규칙 위배라 거부)
    • 대안 3: Event sourcing 전환 (domain events as state)raw/company-tech-blogs/domain-event-sourcing-vs-cqrs-greg-young (Greg Young; ca-tmpl 미채택, "transport-free fact" 정의만 차용)
    • 대안 4: CQRS with separate read/write models — 동일 Greg Young source (ca-tmpl 미채택, read 분리 없이 단일 model 유지)
    • 대안 5: Functional domain modeling (Scala/F#) — JVM이지만 패러다임 차이 + 팀 학습 비용 큼
  • 비교 핵심: ca-tmpl rich model은 Fowler/Vernon reference standard 정합. ORM 외부 매핑(Vernon Option A)이 forbidden import 규칙(domain logger/JPA ban)과 정합 — 우아한형제들 Option B는 same regulation 위배라 거부. Event sourcing/CQRS는 모델 자체 교체로 scope 다름, ca-tmpl은 "transport-free fact" 정의만 차용.

완료 후 wiki 추출 대상

  • wiki/projects/ca-skeleton-operational-contract.md의 domain modeling canonical section.

Audit & Findings

2026-06-05 /branch-spec ground-truth 대조 (ca-tmpl src/ + CleanArchitectureTest.java) 에서 발견한 정합/drift. 사용자 작성 결정 영역은 자동 rewrite 하지 않고 정합 권고만 기록.

ID 유형 발견 권고
F1 OWNERSHIP D1(domain framework-neutral) 의 정적 강제 domain_is_pure 는 본 branch 가 아니라 raw/branch-notes/feature-architecture-enforcement-rules D3 가 owner (CleanArchitectureTest.java:36-57 .as(...) 주석 명시) D1 은 본 branch 가 복제/재정의하지 않고 위임. §Coverage 에 delegated 로 표기 (완료)
F2 GAP (logger ban 미강제) D3(domain logger ban) — domain_is_pure forbidden list 에 logging framework 미포함 (org.slf4j·java.util.logging·logback·log4j 부재; test 전체 grep 상 logger ban rule 없음) logger ban 은 현재 planned, 코드 미강제. C2 에서 별도 rule 또는 forbidden list 확장 필요 (§구현 가이드 2). "구현됐다" 로 말하면 안 됨
F3 NOT-IMPLEMENTED @ValueObject·@AggregateRoot·@DomainEvent annotation 모두 src/ grep 미존재. domain-core 모듈은 identifier/ResourceId·IdFactory 만 보유 D5/D6/D7/D8 의 annotation-기반 ArchUnit rule 은 전부 planned. Claims To Verify 의 planned 표기와 일치 (정합 OK)
F4 SCOPE 확인 D2(domain exception 이 operational error code 를 직접 모름) 의 SSOT 는 feature-operational-error-observability-foundation + error-codes.yaml safe reason enum → error.category 번역은 그 계약 consume. 본 branch 는 domain 측 금지만 소유, code enum 신설은 범위 밖

관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)

/coverage 가 채우는 생성물 — 손유지 금지. 기준: rules/coverage-gate.md. governing_docs: privacy-file-domain-modeling (§"Domain Modeling") + clean-architecture-package-layout (domain purity). 마지막 감사: 2026-06-05 /branch-spec 인라인 (정식 coverage-auditor 판정은 §8b 에서).

관심사 상태 owner 심각도 근거
VO private constructor + factory, invariant in constructor covered-here D5·D6 (§구현 가이드 3, planned)
aggregate root mutator non-public (package-private/protected) covered-here D7 (§구현 가이드 4, planned)
domain layer logger ban covered-here 🟡 (F2 GAP) D3 (planned, 코드 미강제 — §구현 가이드 2)
safe reason enum (거부 사유 noun enum, application 이 로그 번역) covered-here D3·D2
Vernon Option A (ORM 외부 매핑) 채택, Option B 거절 covered-here D5·D7
domain event = transport-free fact, integration mapping 은 경계 covered-here D4·D8
domain framework-neutral (no Spring/JPA/Hibernate) 정적 강제 delegated raw/branch-notes/feature-architecture-enforcement-rules D3 owner actually-implemented (domain_is_pure, CleanArchitectureTest.java:36)
controller 가 domain/entity 타입 직접 반환 금지 delegated raw/branch-notes/feature-boundary-validation-mapping-contract D8 owner actually-implemented (controllers_do_not_return_domain_or_entity_types)

마주친 문제

  • 아직 없음(문서 단계).

묶음

2026-06-05 Phase C2 구현으로 파생 자료 누적. 아래 derived note 들과 양방향 link 유지.

오류 기록 (본 feature 작업 중 발생)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

Blog topics (구현·트러블슈팅 글감)

관련 일일 노트

이 브랜치를 작업한 날짜들. 양방향 nav 유지.

  • (Phase E 외부 근거 / 대안 조사 단계 — daily note 미연결. C2 구현 진입 시 작업일 추가)

완료 후 정리

머지/종료 시점에 채움.

  • PR 링크:
  • 리뷰 메모:
  • 머지 결과 / 배포 환경:
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목:
    • locally-verified 항목:
    • prod-verified 항목:
  • 추출하지 않을 항목 (planned / documented-only / abandoned):