Files
llm-wiki/wiki/projects/ca-tmpl/clean-architecture-package-layout.md
T

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
ca-skeleton
clean-architecture
package-layout
locally-verified
interview-candidate
ca-skeleton
ca-tmpl
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-portfolio 8개 module로 전환되었다. (이후 feature-resource-identifier-contract branch가 9번째 module adapter-identifier를 추가했으나 이는 본 slice 범위 밖이다.)
  • production package root는 dev.caskeleton이다. 기존 reference blog code는 다음 mapping으로 이동했다.
    • cmdapp-bootstrap / dev.caskeleton.bootstrap (BlogApplicationCaSkeletonApplication)
    • domaindomain-core / dev.caskeleton.domain
    • serviceapplication-core / dev.caskeleton.application
    • presentationadapter-web / dev.caskeleton.adapter.web
    • infraadapter-persistence / dev.caskeleton.adapter.persistence
    • blog.* 설정 prefix → ca-skeleton.*, CmdSettingsBootstrapSettings
  • 기존 reference code는 production module에서 격리되어 sample-portfolio 내부 dev.caskeleton.sample.portfolio.{domain,application}.worklog package로 이동했다.
  • 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.gradleverifyCleanArchitectureDependencies task(root build.gradle:53)가 module dependency matrix를 검사한다.
  • app-bootstrapCleanArchitectureTest(ArchUnit)가 domain purity, application adapter isolation, adapter 간 직접 의존 금지, web DTO containment, shared-contract package scope, production → sample-portfolio dependency 금지를 검사한다.
  • (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/module CLAUDE.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 @db61075 ground-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.Transactional FQN에 의존하면 실패. (코드 주석상 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.ApplicationContext FQN에 의존하면 실패. class-literal 기반 getBean(Class<T>) 호출까지는 bytecode access로 catch. (actually-implemented) — 한계(D12): string-key getBean(String)·Class.forName(String)·BeanFactory#getBeansOfType 같은 reflection-style bypass는 ArchUnit 정적 분석으로 catch 불가. application-core/CLAUDE.md forbidden 섹션의 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.worklog package)를 추가했을 때 CleanArchitectureTest가 실패함을 확인한 뒤 임시 파일을 제거했다.
  • 임시 app-bootstrap → sample-portfolio project dependency 선언 시 verifyCleanArchitectureDependencies가 실패함을 확인한 뒤 제거했다.
  • round 2: domain_is_purelombok.. 추가(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-baseline slice가 일부 채움.
  • application/port/inapplication/port/out package anchor는 존재하지만, reference blog repository port는 본 slice 시점엔 sample-portfolio/domain/repository에 남아 있었다. production use case port 정리는 feature-application-port-usecase-contract branch에서 수행한다.
  • 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에서 무엇을 막는지.
  • 적당히 답할 수 있는 질문

    • 왜 Spring Modulith를 즉시 도입하지 않았는지.
    • reference blog port가 아직 domain/repository에 남아 있는 이유와 feature-application-port-usecase-contract branch에서 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-literal getBean(Class)까지만 catch하고, string-key getBean(String) / Class.forName(String) / BeanFactory#getBeansOfType reflection-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로 제한한다.

관련 개념

Sources

Cluster / 묶음