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 |
|
1 | feature-domain-feature-onboarding-contract |
|
|
|
2026-05-22 | in-progress | 80c4d9f23b6ee00310f6c605ffe62bf8caaec262afa9719ecf7fda9c724fed83 |
branch: feature-domain-feature-onboarding-contract
Layer:
raw/branch-notes/— 실제 도메인 기능을 skeleton에 얹을 때 따라야 하는 multi-module onboarding 기준을 정의합니다.
부모 (필수)
- Parent project (canonical SSOT): raw/project-notes/ca-skeleton-operational-contract
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-coredomain model/rule 추가 기준 정의 — 등급:locally-verifiedapplication-coreuse case / command-query / port 추가 기준 정의 — 등급:locally-verifiedadapter-webDTO / mapper / controller / contract test 추가 기준 정의 — 등급:locally-verifiedadapter-persistenceentity / repository / mapper / migration 추가 기준 정의 — 등급:locally-verifiedadapter-outboundoptional adapter 추가 조건 정의 — 등급:documented-only(optional 조건만 정의; 이번 dry-run 에 외부 adapter 없음)shared-contract변경 승인 조건 정의 — 등급:locally-verifiedsample-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-bootstrapArchUnit/JUnit 테스트에DomainFeatureOnboardingContractTest와 test-onlydev.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 D1
D5; 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 =
readOnlytx +READ_REPOSITORYcapability). 추상 Read/Write Difference Table 의 read-only 열을 실제 메커니즘으로 고정.
- UNSUPPORTED_IMPL_DECISION: read 가 DB 를 전혀 안 탈 때 outbound read port 자체를 생략할지 — capability
NONEvsREAD_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)).
- UNSUPPORTED_IMPL_DECISION: ① idempotency 모드(
IDEMPOTENTvsKEYED) —KEYED는 raw/branch-notes/feature-rate-limit-idempotency-contract merge 전까지 금지(raw/branch-notes/feature-application-port-usecase-contract D14), 그 전엔NOT_IDEMPOTENT/IDEMPOTENT만. ② transaction 격리/전파 비기본값 —inWrite(기본) vsinNew(outbox/audit/보상 전용); 비기본 propagation 은 기능별 trade-off 이며 application-port D12(inNew= 새 JDBC connection, loop 호출 금지)를 따른다.
필수 파일 (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.yaml 에 owner_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.gradle이implementation project(':sample-portfolio')를 선언하면 실패 —verifyCleanArchitectureDependencies(allowedProjectDependencies 에서 sample-portfolio 제외) + ArchUnitproduction_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 순서 기준 단조 증가로 고정하고 충돌 시 빌드 실패를 신호로 받는다.
- read-only feature 를 write 파일 없이 추가 →
- 다른 계약 의존 (이 계약이 바뀌면 본 branch 의 onboarding slice 영향):
- raw/branch-notes/feature-skeleton-package-blueprint-contract D1~D9 — module/package boundary·dependency direction. 변경 시 §구현 가이드 §1 placement 표 갱신.
- raw/branch-notes/feature-architecture-enforcement-rules D1~D8 — ArchUnit/Gradle rule 명·범위. rule rename 시 본 노트의 rule 인용 갱신 필요.
- raw/branch-notes/feature-application-port-usecase-contract D1/D3/D9/D14 —
*UseCase/*Port명명,TransactionPort, read-only capability, idempotency 게이트. consume only. - raw/branch-notes/feature-sample-domain-contract-fixture D5 — sample scenario/minimum-model owner. 본 branch 는 구조 참고만.
- raw/branch-notes/feature-implementation-readiness-scorecard D5 — 본 branch checklist 를 consume (역방향 의존). 본 §의 checklist 구조가 바뀌면 scorecard area #15 dry-run 매핑 영향.
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 =*Portco-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.gradle에adapter-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-webcontroller가 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-verified — DomainFeatureOnboardingContractTest.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-verified — use_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-verified — shared_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-verified — verifyCleanArchitectureDependencies, 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에서 파생된 자료)
- raw/company-tech-blogs/feature-first-sahibinden-package-by-layer-vs-feature
- raw/company-tech-blogs/hexagonal-woowahan-techblog-2023
- raw/company-tech-blogs/layer-first-kamilmazurek-github-template
- raw/company-tech-blogs/modulith-arawn-github-modular-monoliths-spring
- raw/company-tech-blogs/modulith-kakaobank-techblog-2025
- raw/company-tech-blogs/onion-allegro-tech-blog-2023
- raw/official-docs/domain-fowler-anemic-vs-rich-model
- raw/official-docs/domain-vaughn-vernon-aggregate-root
- raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011
- raw/official-docs/hexagonal-cockburn-wikipedia-summary
- raw/official-docs/hexagonal-thombergs-buckpal-github
- raw/official-docs/layer-first-baeldung-clean-architecture-spring-boot
- raw/official-docs/modulith-spring-official-doc
- raw/official-docs/onion-palermo-original-2008
본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
Sub-branches (세부 작업)
- (없음)
오류 기록 (이 branch 작업 중 발생)
- raw/errors/gradle-wrapper-sandbox-lock-2026-06-25 — Gradle wrapper/test 실행이 sandbox 밖
~/.gradlelock 파일 쓰기에서 실패한 재현 가능한 도구 문제. - raw/errors/onboarding-fixture-package-path-mismatch-2026-06-25 — synthetic onboarding fixture package와 source path가 불일치해 sampleOffTest 컴파일이 실패한 문제.
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- raw/interviews/clean-architecture-domain-onboarding-guardrails — Clean Architecture 템플릿에서 새 도메인 온보딩을 문서가 아니라 실행 가능한 guardrail 로 검증하는 방법.
강의 (이 작업을 위해 학습한 강의)
- (없음)
job-posting tie-ins (이 작업에서 파생된 글감)
- raw/blog-topics/executable-clean-architecture-onboarding-2026-06-25 — multi-module Clean Architecture onboarding checklist 를 ArchUnit/JUnit dry-run 으로 고정한 경험 글감.
- job-posting tie-ins: 없음.
관련 일일 노트
완료 후 정리
머지/종료 시점에 채움.
/ingest가 이 섹션을 기준으로 wiki/projects/에 추출.
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- wiki 추출 대상 (verified만,
wiki/projects/로만 추출):actually-implemented항목:locally-verified항목:prod-verified항목:
- 추출하지 않을 항목 (planned / documented-only / abandoned):