22 KiB
title, source_type, status, confidence, tags, related_projects, last_reviewed
| title | source_type | status | confidence | tags | related_projects | last_reviewed | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ca-tmpl - Clean Architecture 패키지 레이아웃 결정 | project | verified | high |
|
|
2026-07-02 |
ca-tmpl - Clean Architecture 패키지 레이아웃 결정
Layer:
wiki/projects/— 내 프로젝트 사실. 일반 개념은wiki/concepts/clean-architecture-package-layout사용.
프로젝트 컨텍스트
ca-tmpl은 Java 21 + Spring Boot 3.4 + Gradle multi-module 기반 Clean Architecture skeleton template이다. blog 도메인은 reference implementation이며, 새 프로젝트에서는 도메인 이름과 엔티티를 교체하되 module boundary와 dependency direction은 유지한다.
본 문서는 feature-skeleton-package-blueprint-contract branch-note의 package/module blueprint가 ca-tmpl repo에 실제 반영된 상태를 기록한다. 이 slice는 actually-implemented + locally-verified이며, 운영 배포 대상이 아니므로 prod-verified는 없다. 2026-06-04 ca-tmpl 레포(@5d89766) ground-truth 대조로 아래 사실을 검증함 (§Ground-truth 대조 참조).
실제 구현 내용 (actually-implemented)
- Gradle include가
app-bootstrap,domain-core,application-core,adapter-web,adapter-persistence,adapter-outbound,shared-contract,sample-portfolio8개 module로 전환되었다. (이후feature-resource-identifier-contractbranch가 9번째 moduleadapter-identifier를 추가했으나 이는 본 slice 범위 밖이다.) - production package root는
dev.caskeleton이다. 기존 reference blog code는 다음 mapping으로 이동했다.cmd→app-bootstrap/dev.caskeleton.bootstrap(BlogApplication→CaSkeletonApplication)domain→domain-core/dev.caskeleton.domainservice→application-core/dev.caskeleton.applicationpresentation→adapter-web/dev.caskeleton.adapter.webinfra→adapter-persistence/dev.caskeleton.adapter.persistenceblog.*설정 prefix →ca-skeleton.*,CmdSettings→BootstrapSettings
- 기존 reference code는 production module에서 격리되어
sample-portfolio내부dev.caskeleton.sample.portfolio.{domain,application}.worklogpackage로 이동했다. domain-core,application-core,adapter-persistence,adapter-outbound,shared-contract는 skeleton anchor package +package-info.java중심으로 유지된다. 각 module 내부의 실제 business/contract type 구현은 후속 branch slice들(feature-operational-error-observability-foundation,feature-api-contract-baseline등)이 채운다.src/build.gradle의verifyCleanArchitectureDependenciestask(rootbuild.gradle:53)가 module dependency matrix를 검사한다.app-bootstrap의CleanArchitectureTest(ArchUnit)가 domain purity, application adapter isolation, adapter 간 직접 의존 금지, web DTO containment, shared-contract package scope, production →sample-portfoliodependency 금지를 검사한다.- (D9) module 간 의존 선언 정책: 기본
implementation, 소비자의 public ABI에 타 module 타입이 노출될 때만api. ground-truth 확인: 9개build.gradle모두api선언 0개, 전부implementation— 정책 충족. 근거raw/official-docs/gradle-java-library-api-vs-implementation.md. - (D10)
@SpringBootApplication배치:dev.caskeleton.bootstrap(root package)에 두고 default package 금지. multi-module component scan을 위해@SpringBootApplication(scanBasePackages = "dev.caskeleton")명시. ground-truth 확인:app-bootstrap/.../bootstrap/CaSkeletonApplication.java에 일치. 근거raw/official-docs/spring-boot-structuring-your-code.md. README.md,AGENTS.md, root/moduleCLAUDE.md, local clean-architecture rule이 새 module vocabulary로 갱신되었다.
Boundary enforcement rules (feature-architecture-enforcement-rules slice)
이 sub-section은 module/package blueprint 위에 얹는 enforcement-rules dimension이다. 위 blueprint가 "module 경계가 어디 있는가"라면, 아래는 "그 경계가 깨지면 build가 실패하는가"를 다룬다. ca-tmpl
@db61075ground-truth 대조로 아래 rule 이름·개수·위치를 확인했다(§Ground-truth 대조 — enforcement 참조). ⚠️ ground-truth 파일은 이후 다른 branch slice들이 rule을 더 추가했으므로, 아래는 본 enforcement-rules slice가 정의·구현한 항목만 추렸다(타 slice rule은 해당 branch ingest에서 다룬다).
ArchUnit test(app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java, @AnalyzeClasses(packages = "dev.caskeleton", importOptions = DoNotIncludeTests.class))가 정적 import/dependency graph를 검사한다. 본 slice가 정의한 rule:
domain_is_pure—..domain..이org.springframework../jakarta.persistence../javax.persistence../jakarta.servlet../org.hibernate../lombok..(D3) /..application../..adapter../..bootstrap..등에 의존하면 실패. domain을 framework-neutral POJO로 유지. (actually-implemented)application_does_not_depend_on_adapters_or_transport—..application..이..adapter../..bootstrap../org.springframework.web../ persistence·hibernate에 의존하면 실패. (actually-implemented)application_does_not_use_spring_transactional_annotation—..application..이org.springframework.transaction.annotation.TransactionalFQN에 의존하면 실패. (코드 주석상 attribution은feature-application-port-usecase-contract D3이나, 본 enforcement slice의 테스트 계약에도 포함되어locally-verified로 red/green 확인됨.)application_does_not_depend_on_application_context(D11, banned-class rule) —..application..이org.springframework.context.ApplicationContextFQN에 의존하면 실패. class-literal 기반getBean(Class<T>)호출까지는 bytecode access로 catch. (actually-implemented) — 한계(D12): string-keygetBean(String)·Class.forName(String)·BeanFactory#getBeansOfType같은 reflection-style bypass는 ArchUnit 정적 분석으로 catch 불가.application-core/CLAUDE.mdforbidden 섹션의 code review checklist로만 보완. ArchUnit이 모든 우회를 잡는다고 말하면 과장.web_adapter_does_not_depend_on_persistence_or_outbound_adapters/persistence_adapter_does_not_depend_on_web_or_outbound_adapters/outbound_adapter_does_not_depend_on_web_or_persistence_adapters— adapter module 간 직접 의존 금지. (actually-implemented)web_dtos_stay_in_web_adapter—..adapter.web..dto..는..adapter.web..에서만 접근 가능(DTO containment). (actually-implemented)shared_contract_contains_only_operational_contract_packages—..shared..는 response/request/error/operation/headers/logging/tracing/metrics/registry/annotation operational-contract package allowlist만 허용; business/domain concept 유입 시 실패. (locally-verified— 임시shared.worklog위반으로 red 확인)production_code_does_not_depend_on_sample_portfolio—..sample.portfolio..밖 production code가 sample package에 의존하면 실패. (locally-verified)
Gradle build-graph 검사는 verifyCleanArchitectureDependencies task(src/build.gradle:53, root)가 담당한다. allowedProjectDependencies matrix로 9개 module의 허용된 project() dependency(api/implementation/compileOnly/runtimeOnly)를 화이트리스트하고, 허용 외 ProjectDependency가 선언되면 GradleException을 던진다. ArchUnit이 source import graph를, 이 task가 Gradle project dependency graph를 막는 이중 방어다. (actually-implemented — task 존재 + matrix; locally-verified — 임시 app-bootstrap → sample-portfolio 선언으로 red 확인)
allowEmptyShould(true): 대부분 rule이 빈 anchor module(아직 구현 type이 없는 module)에서 vacuous하게 통과하지 않도록 명시. 빈 should가 곧 PASS로 둔갑하는 ArchUnit empty-should anchor 문제를 다루기 위함.
Negative fixture (violations-as-data)
ArchitectureViolationFixtureTest(같은 architecture/ 패키지)가 본 slice의 각 rule이 실제로 위반을 catch하는지 commit된 negative test로 보증한다(Spring Modulith example/ninvalid 패턴 차용). 본 slice가 추가한 fixture·test(round 2, 2026-05-28): SpringDependentDomainFixture(domain_is_pure D3), ApplicationContextDependentFixture(D11), TransactionalAnnotatedFixture(@Transactional)를 포함한 의도된 위반 class와, 대응 *_catches_violation test가 rule.evaluate(VIOLATION_CLASSES).hasViolation() == true를 assert한다. fixture는 src/test/...에 위치하므로 main @AnalyzeClasses(importOptions = DoNotIncludeTests.class) 분석에서 제외 → main suite의 vacuous pass 위험 없음. (actually-implemented)
이후 branch slice들이 같은 fixture tree에 boundary-validation·streaming·serialization·resource-identifier·api-contract rule용 fixture를 추가해, 현재 ground-truth
ArchitectureViolationFixtureTest는 본 slice 범위를 넘는 negative test를 다수 포함한다. 본 doc은 enforcement-rules slice가 만든 fixture만 위에 명시했다.
로컬/dev 검증 (locally-verified)
2026-05-27 ca-tmpl repo에서 다음 명령이 통과했다.
cd src
./gradlew verifyCleanArchitectureDependencies
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest'
./gradlew :adapter-web:test --tests '*SettingsTest'
./gradlew test
검증 의미:
- Gradle project dependency graph가 branch-note의 module dependency direction을 위반하지 않는다.
- ArchUnit이 source-level forbidden dependency를 검사한다.
- web settings binding tests가 package rename 이후에도 통과한다.
- 전체 Gradle test suite가 새 module layout에서 통과한다.
enforcement-rules slice 추가 red/green 검증(2026-05-28, feature-architecture-enforcement-rules):
cd src
./gradlew :app-bootstrap:test verifyCleanArchitectureDependencies # ArchUnit + Gradle graph
./gradlew check # 전체 — round 2 fixture 포함
- 임시 위반 코드(
application @Transactional, controller domain return, mapper → application 의존,shared.worklogpackage)를 추가했을 때CleanArchitectureTest가 실패함을 확인한 뒤 임시 파일을 제거했다. - 임시
app-bootstrap → sample-portfolioproject dependency 선언 시verifyCleanArchitectureDependencies가 실패함을 확인한 뒤 제거했다. - round 2:
domain_is_pure의lombok..추가(D3)와application_does_not_depend_on_application_context(D11)가 commit된 negative fixture(ArchitectureViolationFixtureTest)로 catch 동작을 보증함을 확인했다.
운영 검증 (prod-verified)
없음. ca-tmpl은 template repository이며, 이번 package blueprint slice는 운영 배포/운영 로그/운영 metric으로 검증된 항목이 아니다.
문서/계획만 존재 (documented-only / planned)
이 등급은 본 blueprint slice 시점(2026-05-27) 기준이다. 일부 항목은 이후 별도 branch slice가 구현했을 수 있으며, 그 검증은 해당 branch의 ingest에서 갱신한다(본 slice는 module/package 경계만 검증).
sample-portfolio실제 worklog domain fixture business flow는 본 slice 시점엔 anchor 중심이었다. (현재dev.caskeleton.sample.portfolio.{domain,application}.worklog에 worklog 모델/테스트 존재 — 별도 sample fixture branch 산출물.)adapter-outbound실제 HTTP client/messaging/cache/notification adapter는 본 slice 시점에 미구현(anchor만).shared-contract실제 response/error/header/logging/tracing/metrics/registry/annotation type은 본 slice 시점에 미구현(anchor만). 이후feature-operational-error-observability-foundation·feature-api-contract-baselineslice가 일부 채움.application/port/in및application/port/outpackage anchor는 존재하지만, reference blog repository port는 본 slice 시점엔sample-portfolio/domain/repository에 남아 있었다. production use case port 정리는feature-application-port-usecase-contractbranch에서 수행한다.- Spring Modulith verifier는 도입하지 않았다. 현재 검증은 Gradle dependency rule + ArchUnit rule이다.
면접에서 말할 수 있는 범위
-
자신 있게 답할 수 있는 질문
- 왜 Gradle multi-module을 1차 boundary로 두고
domain-core/application-core/adapter-*를 물리 분리했는지. shared-contract를 business common dumping ground로 쓰지 않기 위해 어떤 package와 ArchUnit rule을 두었는지.sample-portfolio이 presentation layer가 아니라 fixture/sample consumer module인 이유.verifyCleanArchitectureDependencies와 ArchUnit test가 각각 build graph와 source import graph에서 무엇을 막는지.
- 왜 Gradle multi-module을 1차 boundary로 두고
-
적당히 답할 수 있는 질문
- 왜 Spring Modulith를 즉시 도입하지 않았는지.
- reference blog port가 아직
domain/repository에 남아 있는 이유와feature-application-port-usecase-contractbranch에서application/port/out으로 이동할 계획.
-
답하면 안 되는 질문
- “운영에서 검증했다”는 표현. 운영 배포/운영 metric 근거가 없다.
- “sample-portfolio worklog business flow까지 이 blueprint slice에서 구현했다”는 표현. 본 slice는 module/package 경계만 검증했고, fixture 구현은 별도 slice다.
- “ArchUnit이 모든 boundary 우회를 잡는다”는 표현. runtime lookup/reflection 우회는 별도 리뷰와 CI 보완이 필요하다.
과장 금지 지점
- “ca-tmpl 전체 Phase C2가 완료됐다” → 금지. package/module blueprint slice만 local verification 완료.
- “모든 operational contract가 구현됐다” → 금지. registry/generated constants, outbox, security, runtime, privacy 등은 별도 slice다.
- “Spring Modulith 수준 named interface 검증을 구현했다” → 금지. 현재는 Gradle + ArchUnit 최소 검증이다.
- “prod-verified” → 금지. 운영 환경 검증 없음.
- “
application_does_not_depend_on_application_context(D11) rule이 모든 Spring container 우회를 잡는다” → 금지. class-literalgetBean(Class)까지만 catch하고, string-keygetBean(String)/Class.forName(String)/BeanFactory#getBeansOfTypereflection-style bypass는 ArchUnit 정적 분석 범위 밖이다(D12). 이 부분은 code review checklist로만 보완하며 자동 강제 장치가 아니다. - “ArchUnit/Gradle이 enforcement-rules의 모든 항목을 자동 검증한다” → 금지. MapStruct generated mapper exemption(D9)은
needs-confirmation, runtime lookup false-pass 확인은planned로 남아 있다.
Ground-truth 대조 (2026-06-04, ca-tmpl @5d89766)
실제 레포 대조로 검증한 사실 (locally-verified):
| 검증 항목 | ca-tmpl 증거 |
|---|---|
| 8 module include | settings.gradle 일치 (+ adapter-identifier는 별도 branch) |
production root dev.caskeleton |
CaSkeletonApplication @ dev.caskeleton.bootstrap |
D9 전 module implementation |
9개 build.gradle 모두 api 0개 |
D10 scanBasePackages="dev.caskeleton" |
CaSkeletonApplication.java |
| boundary guardrail | root build.gradle:53 verifyCleanArchitectureDependencies + app-bootstrap/.../architecture/CleanArchitectureTest.java |
anchor package-info.java |
domain-core·shared-contract·adapter-* 존재 |
| sample 격리 | dev.caskeleton.sample.portfolio.*.worklog |
대조에서 정정된 1차 추출 오류: 기존 문서의 com.example.blog.*(→ dev.caskeleton.*), sample-ticket(→ sample-portfolio)은 1차 추출 시점의 stale 값이었고 본 ingest에서 ground-truth로 정정함.
Enforcement-rules dimension 대조 (2026-06-04, ca-tmpl @db61075)
feature-architecture-enforcement-rules slice가 정의한 항목만 실제 레포와 대조함 (actually-implemented / locally-verified):
| 검증 항목 | ca-tmpl 증거 |
|---|---|
| ArchUnit suite 진입점 | app-bootstrap/.../architecture/CleanArchitectureTest.java, @AnalyzeClasses(packages = "dev.caskeleton", importOptions = DoNotIncludeTests.class) |
| domain purity + Lombok ban (D3) | domain_is_pure rule의 forbidden package에 lombok.. 포함 |
| application↔adapter/transport 격리 | application_does_not_depend_on_adapters_or_transport |
| @Transactional ban | application_does_not_use_spring_transactional_annotation (FQN org.springframework.transaction.annotation.Transactional) |
| ApplicationContext banned-class (D11) | application_does_not_depend_on_application_context (FQN org.springframework.context.ApplicationContext) |
| adapter-adapter 격리 | web_/persistence_/outbound_adapter_does_not_depend_on_* 3 rule |
| web DTO containment | web_dtos_stay_in_web_adapter |
| shared-contract scope | shared_contract_contains_only_operational_contract_packages (operational allowlist) |
| production → sample ban | production_code_does_not_depend_on_sample_portfolio |
| Gradle build-graph 검사 | src/build.gradle:53 verifyCleanArchitectureDependencies + allowedProjectDependencies matrix(9 module) |
| negative fixture | ArchitectureViolationFixtureTest + architecture/violations/...(SpringDependentDomainFixture·ApplicationContextDependentFixture·TransactionalAnnotatedFixture 등) |
| D11 한계(string-key bypass) | rule 주석에 명시 — getBean(Class)까지만 catch, getBean(String)/Class.forName 범위 밖 |
⚠️ ground-truth
CleanArchitectureTest는 본 slice 이후 boundary-validation / streaming / serialization / resource-identifier / api-contract slice의 rule도 다수 포함한다(현재 30+ rule). 위 표는 본 enforcement-rules slice 소유 항목만 골랐고, 나머지는 각 branch ingest에서 대조한다.
Blog-topic ingest: clean-architecture-module-blueprint (2026-07-02)
raw/blog-topics/clean-architecture-module-blueprint-2026-05-28 는 Clean Architecture skeleton에서 Gradle module boundary를 1차 강제선으로, package 내부 책임 분류를 2차 강제선으로 둔 이유를 블로그로 풀기 위한 raw seed다.
- locally-verified 로 말할 수 있는 부분: module include, production root,
scanBasePackages, Gradle dependency matrix, package anchor, sample isolation, enforcement-rules slice 검증 범위. - project-local policy 로 말할 부분: Spring Modulith를 즉시 도입하지 않고 Gradle + ArchUnit 최소 검증으로 시작한 선택.
- 블로그 전 과장 방지: 운영 검증이나 전체 Phase C2 완료처럼 쓰지 않고, module/package blueprint slice의 로컬 검증으로 제한한다.
- raw/blog-topics/executable-clean-architecture-onboarding-2026-06-25: Clean Architecture 체크리스트를 README가 아니라 test-only dry-run slice와 negative fixture로 만들어 새 도메인 추가 경계를 CI에서 반복 검증하는 글감. local verification이며 보편 표준 증명처럼 쓰지 않는다.
- raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28: Gradle project-dependency matrix와 ArchUnit bytecode rule을 나눠 Clean Architecture boundary drift를 막는 글감. runtime lookup / MapStruct exemption 같은 planned 항목은 구현 완료로 쓰지 않는다.
- raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28: ArchUnit rule의 vacuous pass를 막기 위해 violations-as-data fixture와 negative test로 rule 자체를 검증하는 글감. static analysis 한계를 보완하는 패턴이지 reflection bypass를 해결하는 것은 아니다.
- raw/blog-topics/archunit-generic-return-type-purity-query-port-2026-06-05:
List<DomainType>같은 generic return type leak을getAllInvolvedRawTypes()로 잡는 query port purity 글감. Object/downcast/reflection 우회까지 잡는다고 쓰지 않는다. - raw/blog-topics/domain-modeling-guardrails-as-archunit-fitness-functions-2026-06-05: DDD marker annotation과 ArchUnit rule로 value object / aggregate / domain event guardrail을 강제하는 글감. marker taxonomy는 ca-tmpl project-local rule로 제한한다.
관련 개념
- wiki/concepts/clean-architecture-package-layout
- wiki/concepts/archunit-scope-classpath-vs-package-filter —
@AnalyzeClasses분석 scope(classpath import vs package filter)와allowEmptyShouldempty-anchor 함정의 일반 지식
Sources
- raw/project-notes/ca-skeleton-operational-contract (§20 Skeleton Blueprint Contract, §29 Topic 1)
- raw/branch-notes/feature-skeleton-package-blueprint-contract
- raw/blog-topics/clean-architecture-module-blueprint-2026-05-28 — module/package blueprint 블로그 글감 raw seed
- raw/branch-notes/feature-architecture-enforcement-rules
- raw/branch-notes/feature-domain-feature-onboarding-contract
- raw/blog-topics/executable-clean-architecture-onboarding-2026-06-25 — executable onboarding guardrails 블로그 글감 raw seed
- raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28 — Gradle + ArchUnit boundary enforcement 블로그 글감 raw seed
- raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28 — violations-as-data ArchUnit fixture 블로그 글감 raw seed
- raw/blog-topics/archunit-generic-return-type-purity-query-port-2026-06-05 — generic return type purity guardrail 블로그 글감 raw seed
- raw/blog-topics/domain-modeling-guardrails-as-archunit-fitness-functions-2026-06-05 — domain modeling guardrail 블로그 글감 raw seed