Files
llm-wiki/raw/branch-notes/feature-domain-feature-onboarding-contract.md

45 KiB

title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, related_projects, governing_docs, 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 related_projects governing_docs tags created target_merge status_label contract_packet_sha256
branch / feature-domain-feature-onboarding-contract branch-note raw BR-CA-SKELETON-OPERATIONAL-CONTRACT-034 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-034
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1
1 feature-domain-feature-onboarding-contract
ca-skeleton
wiki/projects/ca-tmpl/clean-architecture-package-layout
wiki/projects/ca-tmpl/sample-fixture-and-adoption
branch
ca-skeleton
domain-onboarding
module-boundary
clean-architecture
2026-05-22 in-progress 80c4d9f23b6ee00310f6c605ffe62bf8caaec262afa9719ecf7fda9c724fed83

branch: feature-domain-feature-onboarding-contract

Layer: raw/branch-notes/ — 실제 도메인 기능을 skeleton에 얹을 때 따라야 하는 multi-module onboarding 기준을 정의합니다.

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 domain onboarding / sample adoption / implementation readiness 영역을 정제한다.

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: 신규 domain slice가 module·test checklist를 통과한다

상속한 프로젝트 결정

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 slice의 module별 배치와 의존 방향에 적용 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은 도메인 로직을 제거하지만, 실제 프로젝트 시작 시 도메인을 바로 얹을 수 있어야 합니다. Phase C2 기본 구조가 Gradle multi-module로 바뀌었으므로, 새 도메인 기능도 단일 features/{name} 디렉터리가 아니라 domain-core, application-core, adapter-*, shared-contract, sample-portfolio 경계 위에 추가되어야 합니다. controller만 추가하거나 repository만 추가하는 식으로 경계가 무너지지 않도록 최소 onboarding slice를 고정합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • 새 도메인 기능 추가 시 module별 최소 변경 기준.
  • read-only / write use case 차이.
  • domain-core / application-core / adapter-web / adapter-persistence / adapter-outbound 책임 분리.
  • shared-contract 변경이 필요한 조건.
  • sample-portfolio 참조/복제/삭제 기준.
  • onboarding dry-run checklist SSOT.

제외 범위

  • 특정 비즈니스 도메인 선택.
  • code generator 구현.
  • IDE template 제공.
  • Spring Modulith @ApplicationModule 도입.
  • sample-portfolio 실제 scenario 구현. 이 항목은 feature-sample-domain-contract-fixture가 owner.
  • sample-off profile / dual-mode CI matrix / removal lifecycle / reference scaffolding. 이 항목은 feature-sample-removal-adoption-contract가 owner.

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

본 branch의 결정 근거. multi-module 기본값은 company-case-study 근거가 중심이므로, 공식 best practice가 아니라 사례 기반 프로젝트 결정으로 취급한다.

Source 정당화하는 결정
raw/branch-notes/feature-skeleton-package-blueprint-contract Phase C2 기본 module boundary와 dependency direction SSOT
raw/branch-notes/feature-architecture-enforcement-rules onboarding 결과를 Gradle/ArchUnit rule로 검증하는 enforcement 기준
raw/branch-notes/feature-application-port-usecase-contract inbound *UseCase / outbound *Port 명명, TransactionPort, @UseCaseCapability, read-only 캡션 contract SSOT
raw/branch-notes/feature-sample-domain-contract-fixture sample-portfolio scenario/minimum-model owner (본 branch 는 consume only)
raw/branch-notes/feature-sample-removal-adoption-contract sample-off lifecycle / dual-mode CI matrix / removal / reference scaffolding owner (본 branch 는 consume only)
raw/branch-notes/feature-resource-identifier-contract 새 entity PK/ID 생성 정책(ULID server-assigned via domain *IdFactory port) owner — write slice 가 consume
raw/branch-notes/feature-implementation-readiness-scorecard 본 branch 의 dry-run checklist 를 consume 하는 readiness 게이트
raw/company-tech-blogs/woowahan-hexagonal-multimodule Domain / Application / Framework / Bootstrap multi-module hexagonal 사례
raw/company-tech-blogs/modulith-kakaobank-techblog-2025 Gradle multi-module + Hexagonal에서 application/adapter 물리 분리와 Port 통신 사례
raw/official-docs/arch-hexagonal-cockburn port와 adapter 분리의 원형 (engineering-blog, official standard 아님)
raw/official-docs/arch-clean-architecture-uncle-bob Dependency Rule 및 use case 중심 구조 사고 근거 (engineering-blog, official standard 아님)
raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011 framework가 아니라 use case / business 영역이 구조에서 드러나야 한다는 보조 근거
raw/company-tech-blogs/feature-first-sahibinden-package-by-layer-vs-feature layer-first 대비 feature 응집도 사례. module 내부 package 책임 참고용

TODO

각 항목 옆에 증거 등급 표기: actually-implemented | locally-verified | prod-verified | documented-only | planned | needs-confirmation

  • New Domain Module Slice 표를 기준으로 read-only/write onboarding checklist 정의 — 등급: locally-verified
  • domain-core domain model/rule 추가 기준 정의 — 등급: locally-verified
  • application-core use case / command-query / port 추가 기준 정의 — 등급: locally-verified
  • adapter-web DTO / mapper / controller / contract test 추가 기준 정의 — 등급: locally-verified
  • adapter-persistence entity / repository / mapper / migration 추가 기준 정의 — 등급: locally-verified
  • adapter-outbound optional adapter 추가 조건 정의 — 등급: documented-only (optional 조건만 정의; 이번 dry-run 에 외부 adapter 없음)
  • shared-contract 변경 승인 조건 정의 — 등급: locally-verified
  • sample-portfolio을 import하지 않고 구조만 참조하는 dry-run 검증 정의 — 등급: locally-verified

진행 중 메모

  • 기존 문서의 features/{featureName}/{presentation,application,domain,infrastructure} 기준은 2026-05-28부로 이전 기준으로 내린다.
  • 새 기본값은 module-first onboarding이다. 같은 도메인 기능의 파일이 여러 module에 생기더라도 dependency direction이 유지되면 정상이다.
  • onboarding checklist는 실제 code generator가 아니라 review/build 기준이다.
  • 2026-06-15 (branch-spec): 본 노트의 추상 모델(*QueryUseCase, "transaction/idempotency/capability declaration")은 그 이후 ca-tmpl 에서 @UseCaseCapability + @RequiresPermission + TransactionPort 로 구체화됐다. §구현 가이드가 이 실제 메커니즘을 anchor 로 쓰고, drift 는 §Audit & Findings 에 기록한다. capability 어휘 자체의 owner 는 본 branch 가 아니라 feature-application-port-usecase-contract + feature-repository-access-permission-contract(capabilities.yaml).
  • 2026-06-25 (implementation): app-bootstrap ArchUnit/JUnit 테스트에 DomainFeatureOnboardingContractTest와 test-only dev.caskeleton.onboarding.* FeatureAggregate dry-run slice를 추가해 read-only/write onboarding 성공 경로를 검증했다. CleanArchitectureTest에는 repository-backed @UseCaseCapability가 대응 TransactionPort 경계(inRead/inWrite/inNew)를 직접 호출하는지 검사하는 rule을 추가했다.
  • 2026-06-25 (cleanup): dry-run fixture 이름을 Ticket에서 FeatureAggregate로 바꿨다. 이유: app-bootstrap test fixture가 특정 업무 도메인을 skeleton production concept처럼 보이게 만들 수 있어, 온보딩 계약용 중립 명칭으로 정리했다.
  • 2026-06-25 (cleanup): onboarding positive fixture 파일을 src/app-bootstrap/src/test/java/dev/caskeleton/onboarding/ 아래로 이동해 package 선언(dev.caskeleton.onboarding.*)과 파일 경로를 일치시켰다. 이유: bootstrap/architecture/allowed/onboarding 경로와 synthetic package가 어긋나 sampleOffTest 컴파일과 IDE 해석에서 혼선을 만들었기 때문이다.

결정 사항

  • 2026-05-22: 초기 문서의 onboarding 기준은 feature-first package slice였다.
  • 2026-05-28: onboarding 기준을 Gradle multi-module slice로 수정한다. / 이유: Phase C2 기본 구조가 domain-core / application-core / adapter-* / shared-contract / app-bootstrap / sample-portfolio로 바뀜. / 근거: raw/branch-notes/feature-skeleton-package-blueprint-contract.
  • 2026-05-28: 새 도메인 기능의 기본 흐름은 domain model/rule → application use case/port → adapter-web/persistence/outbound 구현 → contract/architecture test 순서로 둔다. / 이유: 안쪽 module이 바깥 adapter를 알지 않게 하기 위함. / 근거: raw/official-docs/arch-clean-architecture-uncle-bob, raw/official-docs/arch-hexagonal-cockburn.
  • 2026-05-28: read-only feature는 write command, idempotency, outbox, persistence mutation을 생략할 수 있다. 단 query use case, inbound port, response mapper, contract test는 필수다. / 근거: project-decision.
  • 2026-05-28: write feature는 command, use case, outbound persistence port, transaction/idempotency decision, persistence adapter, contract test를 함께 추가해야 한다. / 근거: raw/branch-notes/feature-application-port-usecase-contract.
  • 2026-05-28: shared-contract 변경은 response/error/header/logging/tracing/metrics/registry/annotation 같은 skeleton-wide contract일 때만 허용한다. 도메인 전용 타입은 domain-core 또는 adapter DTO에 둔다. / 근거: raw/branch-notes/feature-skeleton-package-blueprint-contract.
  • 2026-05-28: sample-portfolio은 import 대상이 아니라 구조 참고 fixture다. production module이 sample-portfolio을 dependency로 선언하면 실패해야 한다. / 근거: raw/branch-notes/feature-architecture-enforcement-rules.
  • 2026-05-28: dry-run checklist SSOT = 본 branch의 New Domain Module Slice + Read/Write Difference Table. feature-implementation-readiness-scorecard는 consume only로 둔다. / 근거: project-decision.
  • 2026-06-25: onboarding checklist는 문서 표만이 아니라 DomainFeatureOnboardingContractTest의 read-only/write FeatureAggregate dry-run fixture와 ArchUnit negative fixture로 검증한다. / 이유: controller-only 또는 transaction-less write 같은 누락을 리뷰 기억이 아니라 테스트 실패로 잡기 위함. / 검토한 대안: README 체크리스트만 유지. / 근거: project-decision + 로컬 검증(./gradlew test).

New Domain Module Slice

Module Read-only feature Write feature Forbidden
domain-core query response에 필요한 domain model / value object only as needed aggregate/entity/value object/domain rule/domain event as needed Spring/JPA/HTTP DTO/import, adapter type import
application-core query object, *UseCase inbound port, read outbound port if persistence needed, read-only use case command object, *UseCase inbound port, outbound port, use case, transaction/idempotency/capability declaration adapter implementation import, Spring Web/JPA implementation API, direct @Transactional
adapter-web request params/response DTO, mapper, controller, validation error mapping, contract test request DTO, response DTO, mapper, controller, validation, idempotency/header handling, contract test domain object direct response, persistence adapter direct call
adapter-persistence read entity/projection/repository/mapper only if DB read is needed entity/repository/mapper/migration/write adapter implementation controller/web DTO import, application use case import beyond port implementation
adapter-outbound optional; only if read use case calls external dependency optional HTTP/messaging/cache/notification adapter implementation direct adapter-to-adapter coupling
shared-contract normally no change only if new skeleton-wide response/error/header/log/metric/registry contract is required domain-specific type, business enum, feature-specific DTO
app-bootstrap bean wiring/profile update only when needed bean wiring/profile update only when needed domain policy implementation
sample-portfolio reference only; no production dependency reference only; no production dependency production module import/dependency

Read/Write Difference Table

Slice item Read-only Write
inbound port *QueryUseCase or query-specific *UseCase command-specific *UseCase
input model query object or request parameters mapped in adapter command object
outbound port read port only when persistence/external read needed write port required when persistence/external mutation needed
transaction readOnly decision if DB read exists required decision; propagation/isolation explicit when non-default
idempotency normally N/A required decision for retryable external command / create command
domain model/rule as needed required when invariant or state transition exists
adapter-web test response/validation contract response/validation/idempotency/header contract
persistence test query mapping if DB read exists mutation/rollback/constraint mapping
architecture test module boundary + no sample dependency module boundary + no sample dependency + transaction/capability rule

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다. Decision ID 는 이 branch-note 안에서 안정적으로 유지한다. 선택 조건 열(R2): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 N/A.

Decision ID Decision 선택 조건 (언제 이 결정 / 언제 대안) Supporting Claims Evidence Strength Open Risk
D1 onboarding 기준은 Gradle multi-module slice Phase C2 multi-module skeleton 기준일 때. 학습/예제용 single-module 축소형이면 raw/branch-notes/feature-skeleton-package-blueprint-contract D8 의 responsibility-mapping 보존 변환표로 대체 raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C1, raw/company-tech-blogs/modulith-kakaobank-techblog-2025.md#KAKAOBANK-MOD-C4, raw/branch-notes/feature-skeleton-package-blueprint-contract.md company-case-study + project-decision company-tech-blog 사례를 공식 표준으로 승격 금지. ca-tmpl dry-run으로 별도 검증 필요
D2 domain -> application -> adapter 방향으로 추가 N/A (모든 새 도메인 기능 — inner module 이 outer adapter 를 알지 않게) raw/official-docs/arch-clean-architecture-uncle-bob.md, raw/official-docs/arch-hexagonal-cockburn.md#HEX-COCKBURN-ORIG-C3, raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C2 engineering-blog + company-case-study 구체 file set은 ca-tmpl 자체 결정
D3 read-only feature는 write/idempotency/outbox 생략 가능 query-only feature(DB/외부 상태 mutation 없음)일 때 생략. mutation 발생 시 D4 project-decision; raw/branch-notes/feature-application-port-usecase-contract.md (D9: read-only query use case = readOnly tx + READ_REPOSITORY capability) project-decision + sibling-branch-decision read-only 기준이 모호하면 기능별 임의 판단이 생길 수 있음
D4 write feature는 command/use case/port/persistence/transaction/idempotency decision을 함께 요구 state mutation / persistence write 가 있을 때. read-only면 D3 raw/branch-notes/feature-application-port-usecase-contract.md (D1 *UseCase/*Port, D3 TransactionPort, D14 idempotency 게이트) project-decision + sibling-branch-decision TransactionPort 세부 옵션은 아직 needs-confirmation 항목이 남아 있음. idempotency=KEYED 는 raw/branch-notes/feature-rate-limit-idempotency-contract merge 전까지 금지(raw/branch-notes/feature-application-port-usecase-contract D14)
D5 shared-contract는 skeleton-wide operational contract만 허용 새 계약이 skeleton-wide(response/error/header/log/tracing/metrics/registry/annotation)일 때만 변경. domain-specific 타입이면 domain-core 또는 adapter DTO raw/branch-notes/feature-skeleton-package-blueprint-contract.md, raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011.md#SCREAM-C1 project-decision + engineering-blog shared module이 common dumping ground가 될 위험
D6 sample-portfolio은 구조 참고 fixture이며 production dependency 금지 N/A (항상 — production module 의 sample-portfolio dependency 금지) raw/branch-notes/feature-architecture-enforcement-rules.md (D7 production_code_does_not_depend_on_sample_portfolio ArchUnit rule), raw/branch-notes/feature-skeleton-package-blueprint-contract.md project-decision (ArchUnit-enforced) 외부 직접 근거는 약함. Gradle/ArchUnit failure로 실증 필요
D7 readiness scorecard는 본 branch checklist를 consume only N/A (항상 — dry-run checklist SSOT 는 본 branch; scorecard 는 consume) project-decision; raw/branch-notes/feature-implementation-readiness-scorecard.md (D5: real-domain dry-run checklist 가 onboarding branch 를 consume) project-decision + sibling-branch-decision scorecard branch가 자체 checklist를 유지하면 SSOT 충돌 발생
D8 onboarding checklist는 executable dry-run fixture + ArchUnit negative fixture로 검증 ca-tmpl template branch 에서 새 도메인 온보딩 계약을 release-blocking guardrail 로 다룰 때. 단순 문서 안내만 필요한 fork 에서는 문서 체크리스트로 축소 가능 UNSUPPORTED_DECISION — source 는 port/adapter 분리 원칙을 말하지만 test fixture 방식은 ca-tmpl 구현 선택; supporting project evidence: src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/DomainFeatureOnboardingContractTest.java, CleanArchitectureTest.java project-decision + locally-verified ArchUnit 정적 분석은 direct call 만 확인한다. helper 로 숨긴 transaction boundary 는 code review concern

구현 가이드

결정 (Decisions) 이 "무엇 을 할 것인가" 라면, 본 §는 "어디에 어떻게 구현될 것인가" 의 사전 명세. 본 branch 는 통합/소비자 계약 이므로, 각 slice 의 mechanism owner 는 sibling branch 에 있고 여기서는 새 도메인 기능을 얹을 때 module 별로 어떤 파일을 어디에 추가하는가를 고정한다.

Anchor 출처: 모든 경로/클래스/rule 명은 /home/donghyeon/workspace/ca-tmpl @ HEAD 의 실제 코드에서 확인(2026-06-15 branch-spec ground-truth read). 코드 미확인 항목은 planned 로 표기.

1. New domain feature placement & dependency direction

Trace: D1(multi-module slice) + raw/branch-notes/feature-skeleton-package-blueprint-contract D1D5; D2(domain→application→adapter) + raw/branch-notes/feature-architecture-enforcement-rules D1D7. 루트 패키지 dev.caskeleton.*.

  • UNSUPPORTED_IMPL_DECISION: 없음 — module/package 배치와 dependency 방향은 blueprint/enforcement sibling 이 결정·강제(actually-implemented).

새 도메인 기능 <X> 추가 시 module 별 anchor (모두 locally-verified — ArchUnit/Gradle task 가 강제):

Module 추가 위치 (package) 명명 강제 rule (CleanArchitectureTest / Gradle)
domain-core dev.caskeleton.domain.<x>.{model,vo,event,service} @AggregateRoot/@ValueObject/@DomainEvent (dev.caskeleton.domain.stereotype, record) domain_is_pure, domain_has_no_logger, value_objects_have_no_public_no_arg_constructor, aggregate_root_setters_are_not_public, domain_events_are_records
application-core dev.caskeleton.application.{usecase,command,query} (+ outbound *Port interface) inbound *UseCase, outbound *Port (application-port D1) inbound_port_implementations_end_with_use_case, inbound_port_implementations_declare_capability, application_does_not_depend_on_adapters_or_transport
adapter-web dev.caskeleton.adapter.web.{controller,dto,mapper} *Controller(returns Envelope<T>), *Request/*Response DTO controllers_do_not_return_domain_or_entity_types, web_dtos_stay_in_web_adapter, application_methods_do_not_accept_web_dtos, request_dtos_do_not_silence_unknown_fields
adapter-persistence dev.caskeleton.adapter.persistence.<x>.{entity,*JpaRepository,mapper} + src/main/resources/db/migration/V<n>__<x>.sql (Flyway) *Entity(extends AuditableEntity), *JpaRepository persistence_adapter_does_not_depend_on_web_or_outbound_adapters
adapter-outbound dev.caskeleton.adapter.outbound.<x>.* (optional) *Adapter implementing application *Port outbound_adapter_does_not_depend_on_web_or_persistence_adapters, outbound_adapter_method_returns_only_domain_or_primitives
전 module dependency edge Gradle task verifyCleanArchitectureDependencies (src/build.gradle:54-92, allowedProjectDependencies 화이트리스트)

2. Read-only onboarding slice

Trace: D3 + raw/branch-notes/feature-application-port-usecase-contract D9 (read-only query use case = readOnly tx + READ_REPOSITORY capability). 추상 Read/Write Difference Table 의 read-only 열을 실제 메커니즘으로 고정.

  • UNSUPPORTED_IMPL_DECISION: read 가 DB 를 전혀 안 탈 때 outbound read port 자체를 생략할지 — capability NONE vs READ_REPOSITORY 선택은 기능별 trade-off(persistence 의존 0 이면 NONE). application-port 가 원칙만 권고하고 feature 별 detail 은 권고 안 함.

필수 파일 (이 중 하나라도 빠지면 review/build 실패):

추가물 위치/형태 비고
Query 객체 application/query/<X>Query.java implements Query (marker) immutable record
inbound port application/usecase/<X>QueryUseCase.java implements QueryUseCase<Q,R> 이름 ...UseCase 로 끝나야 함 (rule inbound_port_implementations_end_with_use_case)
capability @UseCaseCapability(transactionMode = READ_ONLY, repositoryAccess = READ_REPOSITORY | NONE, idempotency = NOT_IDEMPOTENT) 필수 annotation (rule inbound_port_implementations_declare_capability); 경로 application/capability/UseCaseCapability.java
(선택) read outbound port application/.../<X>ReadPort.java (*Port) DB/외부 read 필요 시에만
response mapper + controller adapter/web/mapper/<X>ResponseMapper, adapter/web/controller/<X>Controller (Envelope<T> 반환) domain object 직접 반환 금지
contract test app-bootstrap/src/test/.../contract/<X>...Test; sample 참조 sample-portfolio/.../WorkLogControllerWireTest·ListRecentWorkLogSummariesUseCaseTest 최소 assert: HTTP 200 + Envelope<T>.data 매핑 + unknown-field 거부(rule request_dtos_do_not_silence_unknown_fields) + domain object 직접 노출 없음

생략 가능 (read-only): Command, idempotency store/executor, outbox, persistence write adapter, @RequiresPermission, TransactionPort.inWrite.

3. Write onboarding slice

Trace: D4 + raw/branch-notes/feature-application-port-usecase-contract D1(*UseCase/*Port)·D3(TransactionPort)·D9·D14(idempotency 게이트). 예시 실증: sample-portfolio/.../application/worklog/CreateWorkLogUseCase.java (@UseCaseCapability(transactionMode = WRITE, idempotency = NOT_IDEMPOTENT, repositoryAccess = WRITE_REPOSITORY)).

필수 파일 (write):

추가물 위치/형태 강제 rule
Command 객체 application/command/<X>Command.java implements Command immutable record
inbound port application/usecase/<X>UseCase.java implements CommandUseCase<C,R> inbound_port_implementations_end_with_use_case
capability @UseCaseCapability(transactionMode = WRITE, repositoryAccess = WRITE_REPOSITORY, idempotency = ...) inbound_port_implementations_declare_capability
permission @RequiresPermission(...) (application/security/RequiresPermission.java) mutating_use_cases_declare_required_permission (WRITE_REPOSITORY ⇒ 필수)
transaction TransactionPort.inWrite(...) 콜백 (application/transaction/TransactionPort.java) — 직접 @Transactional 금지 application_does_not_use_spring_transactional_annotation
outbound write port application/.../<X>WritePort.java (*Port) read_only_use_cases_do_not_call_repository_write_methods(capability 정합)
persistence adapter + migration adapter/persistence/<x>/{<X>Entity, <X>JpaRepository, <X>EntityMapper} + db/migration/V<n>__<x>.sql persistence_adapter_does_not_depend_on_web_or_outbound_adapters
(위임) entity PK/ID 생성 server-assigned ULID via domain *IdFactory port (auto-increment/UUID v4 금지) raw/branch-notes/feature-resource-identifier-contract D5 소관 — 본 branch 범위 밖, consume only
web DTO/mapper/controller + contract test read-only 와 동일 + idempotency/header handling controllers_do_not_return_domain_or_entity_types

4. shared-contract change gate

Trace: D5 + raw/branch-notes/feature-architecture-enforcement-rules D6. shared-contract 는 도메인 기능 추가 시 원칙적으로 변경 없음 — skeleton-wide 계약일 때만.

  • UNSUPPORTED_IMPL_DECISION: 없음 — 허용 package 목록은 ArchUnit rule 이 화이트리스트로 강제.

shared_contract_contains_only_operational_contract_packages 가 허용하는 package 만 변경 가능: error(Category enum 10값 — VALIDATION/AUTH/AUTHZ/NOT_FOUND/CONFLICT/RATE_LIMIT/TRANSIENT_DEPENDENCY/PERMANENT_DEPENDENCY/DATA_INTEGRITY/INTERNAL), response(Envelope<T>), request, operation, headers, logging, tracing, metrics, registry, annotation, security(Permission), concurrency. 도메인 전용 타입/비즈니스 enum/feature DTO 는 금지domain-core 또는 adapter DTO 로. 새 error code 는 docs/registries/error-codes.yamlowner_branch(= 그 기능 branch) + 기존 Category enum 값으로 추가(신규 category 추가는 feature-operational-error-observability-foundation 소관 — 본 branch 범위 밖).

5. sample-portfolio isolation & dry-run

Trace: D6 + raw/branch-notes/feature-architecture-enforcement-rules D7; D7(scorecard consume) + raw/branch-notes/feature-implementation-readiness-scorecard D5. sample scenario/minimum-model 의 owner 는 raw/branch-notes/feature-sample-domain-contract-fixture D5 — 본 branch 는 구조 참고만.

  • UNSUPPORTED_IMPL_DECISION: 없음 — 격리는 ArchUnit + Gradle task 가 강제.
  • production module 의 build.gradleimplementation project(':sample-portfolio') 를 선언하면 실패 — verifyCleanArchitectureDependencies(allowedProjectDependencies 에서 sample-portfolio 제외) + ArchUnit production_code_does_not_depend_on_sample_portfolio.
  • 새 도메인 기능은 sample-portfolio 의 worklog 구조(domain→application→web→persistence 한 슬라이스)를 읽고 모방하되 import 하지 않는다. sample 의 Flyway 는 db/sample-migration/(production 의 db/migration/ 과 분리).
  • dry-run checklist SSOT = 본 branch 의 §New Domain Module Slice + §Read/Write Difference Table + 본 §구현 가이드. [raw/branch-notes/feature-implementation-readiness-scorecard] 는 이를 consume 만 하고 자체 checklist 를 두지 않는다.

구현 결과

Evidence item File / command Result Evidence grade
Read-only onboarding dry-run src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/DomainFeatureOnboardingContractTest.java + dev.caskeleton.onboarding.application.query/ListFeatureAggregatesQuery, FeatureAggregateSummaryQueryPort, ListFeatureAggregatesUseCase, web DTO/mapper/controller fixture query/use case/mapper/controller 존재, write command/write port 부재, ArchUnit rules no violation locally-verified
Write onboarding dry-run dev.caskeleton.onboarding.domain.feature.*, CreateFeatureAggregateCommand, CreateFeatureAggregateUseCase, FeatureAggregateWritePort, persistence entity/mapper/repository adapter, src/app-bootstrap/src/test/resources/db/onboarding-migration/V999__feature_aggregate.sql domain/id factory/command/use case/write port/persistence/migration artifact 존재, ArchUnit rules no violation locally-verified
Transaction boundary enforcement CleanArchitectureTest.use_case_capability_matches_transaction_port_boundary + MissingTransactionBoundaryUseCase negative fixture WRITE_REPOSITORY without TransactionPort.inWrite is caught locally-verified
shared-contract scope enforcement tightened shared_contract_contains_only_operational_contract_packages allowlist + violations/shared/worklog/WorkLogStatus negative fixture domain-specific ..shared.worklog.. package is caught locally-verified
sample isolation DomainFeatureOnboardingContractTest verifies onboarding fixtures have no sample-portfolio dependency; existing SampleRemovalSmokeContractTest keeps production Gradle sample deps test-scoped production/sample boundary remains guarded locally-verified
Neutral fixture naming Ticket* test fixture names renamed to FeatureAggregate*; migration renamed to V999__feature_aggregate.sql onboarding fixture no longer reads as a concrete skeleton domain locally-verified
Package-path alignment onboarding positive fixture moved to src/app-bootstrap/src/test/java/dev/caskeleton/onboarding/; package declarations remain dev.caskeleton.onboarding.* source path now matches package and both test/sampleOffTest compile outputs contain onboarding classes locally-verified

Verification commands (2026-06-25)

Command Result
./gradlew verifyCleanArchitectureDependencies BUILD SUCCESSFUL
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' BUILD SUCCESSFUL
./gradlew :app-bootstrap:test --tests '*DomainFeatureOnboardingContractTest' --tests '*ArchitectureViolationFixtureTest' BUILD SUCCESSFUL
./gradlew test BUILD SUCCESSFUL
./gradlew check BUILD SUCCESSFUL (checkstyle/SpotBugs report output remains non-fatal under current Gradle settings)
./gradlew :app-bootstrap:spotlessCheck :app-bootstrap:test --tests '*DomainFeatureOnboardingContractTest' --tests '*ArchitectureViolationFixtureTest' BUILD SUCCESSFUL after neutral fixture rename
./gradlew :app-bootstrap:compileTestJava :app-bootstrap:compileSampleOffTestJava --rerun-tasks BUILD SUCCESSFUL after package-path alignment
./gradlew :app-bootstrap:test --tests '*DomainFeatureOnboardingContractTest' --tests '*ArchitectureViolationFixtureTest' :app-bootstrap:sampleOffTest --tests '*DomainFeatureOnboardingContractTest' --tests '*ArchitectureViolationFixtureTest' BUILD SUCCESSFUL after package-path alignment
./gradlew check BUILD SUCCESSFUL after package-path alignment

엣지·실패·의존

R4(깊이 게이트) 캡처용. 정상 경로 외에 구현 중 부딪힐 실패/엣지/다른 계약 의존을 미리 열거.

  • 실패·엣지 경로 (각 경로 = 어느 rule 이 잡는가):
    • read-only feature 를 write 파일 없이 추가 → verifyCleanArchitectureDependencies + ArchUnit 통과해야 정상(Claim 1). 반대로 controller 만 추가하고 query use case/mapper 가 없으면 review 실패(테스트 계약).
    • write feature 에서 @UseCaseCapability 누락 → inbound_port_implementations_declare_capability 실패. @RequiresPermission 누락(WRITE_REPOSITORY) → mutating_use_cases_declare_required_permission 실패. 직접 @Transactional 사용 → application_does_not_use_spring_transactional_annotation 실패.
    • capability 와 실제 호출 불일치(예: READ_REPOSITORY 인데 save/delete 호출) → read_only_use_cases_do_not_call_repository_write_methods 실패. bulkWrite=true 인데 WRITE_REPOSITORY 아님 → bulk_write_capability_requires_write_repository_access 실패.
    • controller 가 domain/JPA entity 직접 반환 → controllers_do_not_return_domain_or_entity_types 실패. application 메서드가 web DTO 수신 → application_methods_do_not_accept_web_dtos 실패.
    • shared-contract 에 domain type 유입 → shared_contract_contains_only_operational_contract_packages 실패. jakarta.validation 을 domain/application 에서 import → validation_constraints_stay_at_web_boundary 실패.
    • idempotency KEYED 사용 시도 → 계약 게이트 위반(application-port D14, feature-rate-limit-idempotency-contract 미merge). 빌드가 아니라 review/계약 차원에서 차단.
    • empty anchor 엣지: 새 module/feature 의 빈 anchor package 가 ArchUnit "empty should" 로 오탐될 수 있음 → 유효 rule 에 allowEmptyShould(true) (선례: raw/errors/archunit-empty-should-anchor-2026-05-27).
    • Flyway version 충돌 엣지: 동시 onboarding 중인 두 write feature 가 같은 db/migration/V<n>__*.sql 번호를 잡으면 startup/flywayValidate 실패(실코드 V1/V3/V4 이미 점유). 번호 할당은 merge 순서 기준 단조 증가로 고정하고 충돌 시 빌드 실패를 신호로 받는다.
  • 다른 계약 의존 (이 계약이 바뀌면 본 branch 의 onboarding slice 영향):

Audit & Findings (2026-06-15 branch-spec — ca-tmpl ground-truth 대조)

ca-tmpl 실코드 대조에서 발견한 노트↔구현 drift. 본 branch 결정 영역 의 것은 자동 rewrite 하지 않고 정합 권고만 남긴다.

  • DRIFT① — 추상 capability 모델 → @UseCaseCapability 구체화: 노트의 New Domain Module Slice/Read-Write Table 은 "transaction/idempotency/capability declaration" 을 추상 서술. 실제 ca-tmpl 은 @UseCaseCapability(transactionMode, idempotency, repositoryAccess, externalOutboundAllowed, sensitiveRead, bulkWrite, crossTenantAdmin) + @RequiresPermission + TransactionPort 로 구체화(노트 created 2026-05-22 < capabilities.yaml 2026-06-05). 판정: 추상 표는 contract 로 유효하게 유지, §구현 가이드가 구체 메커니즘을 anchor. capability 어휘 자체의 owner 는 feature-application-port-usecase-contract + feature-repository-access-permission-contract(capabilities.yaml) — OUT_OF_BRANCH_SCOPE, 본 branch 에서 재결정 안 함.
  • DRIFT② — port/in·port/out 트리 미실현: blueprint 계획 트리는 application/port/in·port/out. 실제 application-core 는 usecase/,command/,query/,capability/,transaction/,idempotency/,security/ (inbound port = usecase/*UseCase, outbound = *Port co-located). owner = raw/branch-notes/feature-application-port-usecase-contract D1. OUT_OF_BRANCH_SCOPE — 본 §구현 가이드는 실제 경로(usecase/)를 anchor 로 사용.
  • DRIFT③ — 9번째 module adapter-identifier: 노트의 New Domain Module Slice 는 8 module. 실제 settings.gradleadapter-identifier(ID 생성, feature-resource-identifier-contract 소관) 추가됨. 새 도메인 기능이 보통 건드리지 않음. OUT_OF_BRANCH_SCOPE — 각주로만: "adapter module 은 책임별 확장 가능(예: adapter-identifier)".

테스트 계약

  • 새 read-only feature가 query use case / inbound port / response mapper / contract test 없이 controller만 추가되면 실패.
  • 새 write feature가 command / use case / outbound port / persistence adapter / transaction decision 중 하나 없이 추가되면 실패.
  • domain-core가 Spring/JPA/HTTP DTO/adapter type을 import하면 실패.
  • application-core가 adapter implementation을 직접 import하면 실패.
  • adapter-web controller가 domain object를 response로 직접 반환하면 실패.
  • shared-contract에 domain-specific class/package가 추가되면 실패.
  • production module이 sample-portfolio에 의존하면 실패.

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. 구현 전/중/후에 실제로 검증해야 하는 주장을 분리한다.

Claim Why uncertain How to verify Status
read-only domain onboarding이 write-only 파일 없이도 contract/architecture test를 통과한다 read-only file set은 ca-tmpl 자체 결정 가상 read-only feature 추가 → command/idempotency/outbox 없음 → Gradle/ArchUnit/contract test 통과 확인 locally-verifiedDomainFeatureOnboardingContractTest.read_only_onboarding_slice_has_minimum_contract_and_no_write_artifacts, ./gradlew test
write domain onboarding에서 command/use case/port/persistence/transaction decision 중 하나가 빠지면 실패한다 누락 탐지는 custom ArchUnit/contract rule 필요 violating write feature 추가 → 누락 유형별 실패 메시지 확인 (capability/permission/transaction rule) locally-verifieduse_case_capability_matches_transaction_port_boundary, 기존 capability/permission fixture tests, ./gradlew test
shared-contract에 domain-specific class가 들어오면 실패한다 shared module scope rule 구현 필요 shared-contract/.../worklog/WorkLogStatus 추가 → ArchUnit shared_contract_contains_only_operational_contract_packages 실패 확인 locally-verifiedshared_contract_scope_rule_catches_domain_specific_shared_package, ./gradlew test
production code가 sample-portfolio을 import하면 실패한다 sample 격리는 project decision이며 실증 필요 production module에 implementation project(':sample-portfolio') 추가 → verifyCleanArchitectureDependencies 실패 확인 locally-verifiedverifyCleanArchitectureDependencies, production_code_does_not_depend_on_sample_portfolio, onboarding fixture no-sample assertion
adapter-web controller가 domain object를 response로 직접 반환하면 실패한다 module boundary만으로는 direct return을 잡지 못할 수 있음 controller violating method 추가 → ArchUnit controllers_do_not_return_domain_or_entity_types 실패 확인 locally-verified — existing DomainReturningControllerFixture negative test + onboarding controller no-violation test
scorecard area #15가 본 branch의 checklist를 consume only로 유지한다 cross-branch governance는 자동 강제가 어려움 raw/branch-notes/feature-implementation-readiness-scorecard`에서 자체 dry-run checklist가 없는지 grep 검증 (해당 branch D5 가 본 branch 를 consume 으로 선언함을 확인) locally-verified — `rg -n 'dry-run checklist

마주친 문제

  • Gradle wrapper sandbox lock: 최초 focused test 실행이 ~/.gradle/.../gradle-9.0.0-bin.zip.lck (Read-only file system) 로 실패해 권한 상승으로 재실행했다. 별도 기록: raw/errors/gradle-wrapper-sandbox-lock-2026-06-25.
  • Onboarding fixture package-path mismatch: FeatureAggregate* fixture의 package 선언과 파일 경로가 어긋나 compileSampleOffTestJava에서 패키지를 찾지 못했다. fixture를 src/app-bootstrap/src/test/java/dev/caskeleton/onboarding/ 아래로 이동해 해결했다. 별도 기록: raw/errors/onboarding-fixture-package-path-mismatch-2026-06-25.
  • zsh quoting 실수: rg 패턴에 backtick 을 double quote 안에 넣어 command not found: adoption 이 발생했다. single quote 로 재실행해 scorecard consume-only evidence 를 확인했다.

묶음 (이 branch에서 파생된 자료)

본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.

Sub-branches (세부 작업)

  • (없음)

오류 기록 (이 branch 작업 중 발생)

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

강의 (이 작업을 위해 학습한 강의)

  • (없음)

job-posting tie-ins (이 작업에서 파생된 글감)

관련 일일 노트

완료 후 정리

머지/종료 시점에 채움. /ingest가 이 섹션을 기준으로 wiki/projects/에 추출.

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