Files
llm-wiki/raw/branch-notes/feature-architecture-enforcement-rules.md

50 KiB

title, source_type, status, branch, parent_branch, related_projects, tags, created, last_reviewed, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
title source_type status branch parent_branch related_projects tags created last_reviewed target_merge status_label id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256
branch / feature-architecture-enforcement-rules branch-note verified feature-architecture-enforcement-rules
ca-skeleton
branch
ca-skeleton
architecture
enforcement
archunit
clean-architecture
2026-05-21 2026-06-04 master review BR-CA-SKELETON-OPERATIONAL-CONTRACT-018 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-018
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-ARCHTEST-001@1
1 a1912f62e082b02487a0a332eef101b12c056ac485ec9d1bbb42e4e1590ecb05

Ground-truth 대조 (2026-06-04, ca-tmpl @db61075): 본 branch가 정의한 enforcement rule이 실제 레포에 반영됨을 확인. app-bootstrap/.../architecture/CleanArchitectureTest.javadomain_is_pure(Lombok ban 포함, D3), application_does_not_depend_on_adapters_or_transport, application_does_not_use_spring_transactional_annotation, application_does_not_depend_on_application_context(D11, banned-class), adapter-adapter 격리 3종, web_dtos_stay_in_web_adapter, shared_contract_contains_only_operational_contract_packages, production_code_does_not_depend_on_sample_portfolio 존재. src/build.gradle:53 verifyCleanArchitectureDependencies + allowedProjectDependencies matrix(9 module) 존재. ArchitectureViolationFixtureTest + architecture/violations/에 negative fixture 존재(SpringDependentDomainFixture·ApplicationContextDependentFixture·TransactionalAnnotatedFixture 포함). D11 string-key bypass(D12)는 rule 주석에 한계로 명시됨 — getBean(Class)까지만 catch. wiki/projects/ca-tmpl/clean-architecture-package-layout에 enforcement dimension 추출 완료. ⚠️ ground-truth CleanArchitectureTest는 이후 다른 branch slice rule도 다수 포함(현재 30+ rule)하므로, 추출은 본 branch 소유 항목만 한정함.

branch: feature-architecture-enforcement-rules

Layer: raw/branch-notes/ — Clean Architecture 경계와 skeleton 계약을 architecture test로 강제하는 기준을 정의합니다.

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 §20 Skeleton Blueprint Contract 와 §25 Critical Defaults 의 architecture enforcement 영역을 정제한다.

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: forbidden module/import fixture가 ArchUnit gate에서 실패한다

상속한 프로젝트 결정

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 책임을 분리한다 Work Item 완료 조건에 적용 raw/project-notes/ca-skeleton-operational-contract
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-ARCHTEST-001@1 architecture test는 archunit-junit5 1.3.0을 사용한다 Work Item 완료 조건에 적용 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

목표

문서 기준만으로는 시간이 지나면 module dependency, application boundary, domain purity, adapter boundary, sample isolation 이 무너집니다. feature-skeleton-package-blueprint-contract가 Gradle multi-module 구조를 기본값으로 고정했으므로, 본 branch는 그 구조가 실제 코드에서 깨지면 Gradle/ArchUnit test가 실패하도록 강제 기준을 정의합니다.

  • 이슈: (없음 — local branch, 이슈 트래커 미사용)
  • PR: (미생성 — local verification only, not merged)

범위

포함 범위

  • Gradle multi-module dependency rule.
  • domain-core framework import 금지.
  • application-core -> adapter-* / app-bootstrap 의존 금지.
  • adapter module 간 직접 의존 금지.
  • shared-contract business/domain concept 오염 방지.
  • sample-portfolio production 역수입 금지.
  • mapper boundary rule.
  • transaction annotation forbidden import rule.
  • ArchUnit rule 위치와 실행 기준.

제외 범위

  • formatter / style lint 규칙.
  • business package naming 강제.
  • Spring Modulith verifier 도입.
  • SonarQube custom rule 구현.
  • CI workflow job 분리 구현. CI 실행 시점은 feature-ci-quality-gates-contract에서 최종화.

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

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

Source 정당화하는 결정
raw/official-docs/archunit-user-guide ArchUnit rule / @ArchTest / dependency check 구현 근거
raw/official-docs/governance-archunit-official architecture rule을 test로 강제하는 기본 근거
raw/official-docs/archunit-conditional-on-property-3-layer-pattern predicate/condition 기반 ArchUnit fitness function 근거
raw/official-docs/arch-clean-architecture-uncle-bob Dependency Rule 및 framework-independent domain 사고 근거 (engineering-blog, official standard 아님)
raw/official-docs/arch-hexagonal-cockburn ports/adapters inside/outside asymmetry 근거 (engineering-blog, official standard 아님)
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/modulith-spring-official-doc Spring Modulith verifier 대안. Phase C2 기본값은 아니며 후속 검토 후보
raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011 feature/use-case 중심 구조가 framework 중심 구조보다 의도를 드러낸다는 보조 근거
raw/company-tech-blogs/feature-first-sahibinden-package-by-layer-vs-feature layer-first 대비 feature 응집도 사례. module 내부 package 책임 참고용
raw/official-docs/mapstruct-generated-annotation-official D9: MapStruct generated mapper에 @Generated annotation이 붙는다는 공식 근거 (MS-ANNOT-C1, MS-ANNOT-C2)
raw/official-docs/lombok-builder-data-features-official D3: domain-core Lombok 금지 결정 — @Builder 가 inner static class·setter 등 7가지를 생성하고 @Data 가 setter 를 포함한 full boilerplate 를 생성함을 공식 문서로 뒷받침 (LMB-C1~LMB-C5)
raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data D9 corroboration: Spring Modulith 자체가 annotatedWith(Generated.class) ArchUnit predicate 를 production 코드에 사용 (SPRING-MOD-AU-C1). S1 negative test fixture pattern: detectViolations() returns Violations as data + example/ninvalid fixture package (SPRING-MOD-AU-C2). D8 CONTRARY: @ApplicationModuleListener meta-annotation (SPRING-MOD-TX-C1)
raw/company-tech-blogs/buckpal-archunit-lombok-allowlist-direct-transactional D3 CONTRARY: Buckpal domain purity ArchUnit rule 이 lombok.. 명시 allowlist (BUCKPAL-LOMBOK-C1, C2). D8 CONTRARY: @Component @Transactional 직접 부착 (BUCKPAL-TX-C1, C2). Hexagonal 공식 reference 가 ca-tmpl 결정과 정반대 방향임을 기록

TODO

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

  • Gradle project dependency rule을 domain-core, application-core, adapter-*, app-bootstrap, sample-portfolio 기준으로 정리 — 등급: locally-verified
  • ArchUnit domain-core forbidden import rule 정의 — 등급: actually-implemented
  • ArchUnit application-core adapter dependency forbidden rule 정의 — 등급: actually-implemented
  • adapter module 간 직접 의존 금지 rule 정의 — 등급: actually-implemented
  • shared-contract 허용 package scope rule 정의 — 등급: locally-verified
  • sample-portfolio production 역수입 금지 rule 정의 — 등급: locally-verified
  • mapper boundary / direct domain response 금지 rule 정의 — 등급: locally-verified
  • transaction annotation forbidden import rule 정의 — 등급: locally-verified

진행 중 메모

  • architecture test 기본 도구는 ArchUnit으로 둔다.
  • Gradle dependency graph 검증은 feature-skeleton-package-blueprint-contractverifyCleanArchitectureDependencies와 같은 방향으로 둔다.
  • Spring Modulith verifier는 기본값이 아니라 후속 검토 후보로 둔다. 현재 기본 강제선은 Gradle dependency rule + ArchUnit rule이다.
  • 2026-05-28 구현 반영: ca-tmpl src/build.gradleverifyCleanArchitectureDependencies를 보강하고, src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java에 transaction annotation, controller direct domain response, mapper boundary, shared-contract package allowlist 규칙을 추가했다.
  • 2026-05-28 red/green 검증: 임시 위반 코드로 application @Transactional, controller domain return, mapper -> application dependency, shared.worklog package 위반이 CleanArchitectureTest에서 실패함을 확인한 뒤 임시 파일을 제거했다. 임시 app-bootstrap -> sample-portfolio project dependency도 verifyCleanArchitectureDependencies에서 실패함을 확인한 뒤 제거했다.
  • 2026-05-28 전체 검증: cd src && ./gradlew :app-bootstrap:test verifyCleanArchitectureDependencies, cd src && ./gradlew test 모두 성공. Gradle 10 호환성 deprecation warning은 기존 빌드 경고로 남아 있다.
  • 2026-05-28 워크플로우 반영: ca-tmpl repo 내부 AGENTS.md, CLAUDE.md, .agents/plugins/ca-superpowers/, .claude/, .codex/ 지침에 구현 완료 후 LLM Wiki branch-note 갱신과 raw/errors, raw/interviews, raw/blog-topics 파생 문서 캡처 규칙을 추가했다. 등급: documented-only (repo-local workflow docs; 자동 강제 장치 아님).
  • 2026-05-28 round 2 구현 반영 (D3 Lombok + D11 ApplicationContext + violations-as-data fixture):
    • domain_is_pure rule 의 forbidden packages 에 lombok.. 추가 (D3) → Lombok 사용 시 ArchUnit 실패.
    • application_does_not_depend_on_application_context ArchUnit rule 신규 추가 (D11) → getBean(Class) class-literal 호출까지 catch. String-key bypass (getBean(String), Class.forName(String)) 는 D12 의 code review checklist 한계로 명시.
    • src/app-bootstrap/src/test/java/.../violations/ 패키지에 의도된 위반 fixture 6종 + ArchitectureViolationFixtureTest 6 negative test 추가 → 각 rule 이 실제로 위반을 catch 하는지 commit 된 negative test 로 보증 (Spring Modulith example/ninvalid 패턴).
    • testCompileOnly 'org.springframework:spring-tx'app-bootstrap/build.gradle 에 추가 (TransactionalAnnotatedFixture@Transactional 을 import 하기 위해 — production 영향 없음).
    • 전체 검증: cd src && ./gradlew check PASS, CleanArchitectureTest 14 tests + ArchitectureViolationFixtureTest 6 tests.
  • 2026-06-30 develop 머지 충돌 및 규칙 수정:
    • master에 직접 커밋된 mappers_do_not_depend_on_web_or_application_boundaries 규칙의 패키지 필터(..mapper..)가 develop에 추가된 웹 매퍼(WorkLogWebMapper, FeatureAggregateResponseMapper 등)를 침범하여 테스트가 실패하는 현상이 발생함.
    • 웹 매퍼는 프레임워크/웹 DTO와 애플리케이션 커맨드를 매핑해야 하므로 웹/애플리케이션 의존성이 허용되어야 함.
    • 따라서 해당 규칙의 타겟 패키지를 ..adapter.persistence..mapper..(영속성 매퍼)로 제한함.
    • 또한 Clean Architecture 상 영속성 어댑터는 애플리케이션 코어 레이어를 의존할 수 있으므로(예: 멱등성 매퍼가 애플리케이션 레코드 타입을 참조하는 경우), 영속성 매퍼가 금지해야 할 대상에서 ..application..을 제외하고 ..adapter.web....bootstrap..만 금지하도록 규칙을 수정함.
    • 수정 후 CleanArchitectureTest 54개 테스트 통과 완료.

결정 사항

  • 2026-05-21: CA 경계는 문서가 아니라 테스트로 강제되어야 함. / 이유: 문서만으로는 시간이 지나며 boundary drift가 발생함. / 검토한 대안: (a) 문서 + PR 리뷰만으로 강제 — boundary drift 누적, (b) SonarQube custom rule — out of scope §범위, (c) Spring Modulith verifier — out of scope §범위. / 근거: raw/official-docs/governance-archunit-official.

  • 2026-05-27: package rule은 기존 features.{featureName}.{presentation,application,domain,infrastructure} 기준에서 Gradle multi-module 기준으로 수정. / 이유: Phase C2 기본 구조가 domain-core / application-core / adapter-* / shared-contract / app-bootstrap / sample-portfolio로 바뀜. / 검토한 대안: 기존 features.{featureName}.{layer} package-convention 유지 (single-module 가정) — 채택 안 함. company-case-study (woowahan, kakaobank) 가 모두 module boundary 분리를 택했음. / 근거: raw/branch-notes/feature-skeleton-package-blueprint-contract.

  • 2026-05-27: domain-core는 Spring/JPA/HTTP/adapter type import 금지. / 이유: domain model을 framework-neutral POJO로 유지하기 위함. / 검토한 대안: (a) framework 허용 + DI 패턴으로만 격리 — domain lifecycle 이 framework 에 결합, (b) package-private convention 만 사용 — multi-module 환경에서는 module boundary 가 더 강한 격리 제공. / 근거: raw/company-tech-blogs/woowahan-hexagonal-multimodule, raw/official-docs/arch-clean-architecture-uncle-bob.

  • 2026-05-27: application-coreadapter-*app-bootstrap에 의존하면 안 됨. / 이유: application core가 outbound implementation을 직접 알면 port boundary가 무너짐. / 검토한 대안: Spring Modulith @ApplicationModule named interface 로 module 내부 의존 허용 + 외부 노출만 차단 — out of scope §범위. / 근거: raw/company-tech-blogs/modulith-kakaobank-techblog-2025, raw/official-docs/arch-hexagonal-cockburn.

  • 2026-05-27: adapter module끼리 직접 의존하지 않음. / 이유: adapter 간 공유가 필요하면 application port 또는 shared operational contract로 승격해야 함. / 검토한 대안: (a) adapter-common shared module 생성 — common dumping ground 위험 (D6 와 동일 risk), (b) Spring Modulith named interface — out of scope §범위. / 근거: raw/branch-notes/feature-skeleton-package-blueprint-contract.

  • 2026-05-27: shared-contract는 response/error/header/logging/tracing/metrics/registry/annotation 같은 skeleton-wide operational contract만 허용. / 이유: business common dumping ground를 막기 위함. / 검토한 대안: shared-business 별도 module 신설하여 business common 허용 — 채택 안 함. 사례(woowahan, kakaobank) 모두 shared = operational contract 만 정의. / 근거: raw/branch-notes/feature-skeleton-package-blueprint-contract.

  • 2026-05-27: sample-portfolio은 production module이 import하거나 dependency로 선언하면 실패. / 이유: sample은 production feature가 아니라 contract fixture임. / 검토한 대안: sample 을 production module 과 통합 (sample 분리 안 함) — 채택 안 함. sample 코드가 production 코드 경로에 섞이면 제거 시점 식별 불가. / 근거: raw/branch-notes/feature-skeleton-package-blueprint-contract.

  • 2026-05-22: application layer의 Spring @Transactional 직접 import는 금지하고 transaction abstraction 사용 여부를 검증. / 이유: transaction boundary를 application use case 책임으로 두되 Spring annotation 의존을 숨기기 위함. / 검토한 대안: (a) @Transactional 직접 허용 — Spring 공식 지원, ca-tmpl 은 격리를 위한 소수파 선택(D8 Open Risk), (b) AOP custom annotation 으로 동일 효과 — 추가 추상화 비용, (c) TransactionTemplate programmatic — boilerplate 증가. / 근거: raw/branch-notes/feature-application-port-usecase-contract.

  • 2026-05-22: MapStruct 사용 시 generated mapper package/path exemption을 명시해야 하며 exemption 없는 generated code 우회는 실패. / 이유: generated code가 architecture rule을 무력화하지 않게 하기 위함. / 검토한 대안: MapStruct generated code 에도 rule 적용 (exemption 없음) — build path 분리 검사 필요, 실현 가능성 미검증. / 근거: raw/official-docs/mapstruct-generated-annotation-official MS-ANNOT-C1 (MapStruct가 @Generated annotation을 generated mapper에 부착함을 공식 확인). ArchUnit predicate 구현 방법은 raw/official-docs/archunit-user-guide 보강 필요. ca-tmpl 실제 generated path 확인은 needs-confirmation.

  • 2026-05-22: ArchUnit fail mode = strict-break for new violations. legacy 코드 적용 시 FreezingArchRule baseline 1회 capture 허용, baseline 외 새 violation은 PR block. / 이유: strict-break 가 boundary drift 누적 차단의 핵심. legacy baseline 은 도입 비용을 줄이는 한시적 타협. / 검토한 대안: (a) warning-only mode (CI 비차단) — drift 누적 위험, (b) report-only baseline (legacy 전체 면제) — 신규 위반 강제 불가. / 근거: raw/official-docs/archunit-user-guide.

  • 2026-05-28: non-trivial 구현 종료 조건에 LLM Wiki capture를 포함한다. / 이유: 코드 구현 후 branch-note와 파생 자료 작성을 매번 대화로 요청해야 하는 반복 비용을 줄이고, 구현 사실·검증·트러블슈팅·면접/블로그 후보를 누락 없이 raw 계층에 남기기 위함. / 검토한 대안: (a) 사용자가 매번 수동 요청 — 누락 위험, (b) LLM Wiki vault 규칙만 유지 — ca-tmpl 작업자가 종료 조건으로 인식하지 못함, (c) ca-tmpl repo-local rule로 연결 — 채택. / 근거: 사용자 워크플로우 요구 + raw/branch-notes/feature-architecture-enforcement-rules 본 작업 기록.

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다. Decision ID 는 이 branch-note 안에서 안정적으로 유지한다.

Decision ID Decision Supporting Claims Evidence Strength Open Risk
D1 CA 경계는 architecture test로 강제 raw/official-docs/governance-archunit-official.md#AU-OFF-C1, raw/official-docs/governance-archunit-official.md#AU-OFF-C2, raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C5 official-vendor-doc + engineering-blog ArchUnit은 정적 검사만 가능. runtime lookup / reflection 우회는 별도 보완 필요
D2 package rule은 Gradle multi-module boundary 기준 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 build graph로 별도 검증 필요
D3 domain-core forbidden import rule (Lombok 포함) raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C1, raw/official-docs/arch-clean-architecture-uncle-bob.md, raw/official-docs/lombok-builder-data-features-official.md#LMB-C1, raw/official-docs/lombok-builder-data-features-official.md#LMB-C2, raw/official-docs/lombok-builder-data-features-official.md#LMB-C4, raw/official-docs/lombok-builder-data-features-official.md#LMB-C5 / CONTRARY EVIDENCE: raw/company-tech-blogs/buckpal-archunit-lombok-allowlist-direct-transactional.md#BUCKPAL-LOMBOK-C1, #BUCKPAL-LOMBOK-C2 (Buckpal hex-arch 공식 reference 가 domain purity rule 에서 lombok.. 명시 allowlist — 정반대 방향) company-case-study + engineering-blog + official-vendor-doc + UNSUPPORTED_DECISION (CONTRARY) LMB-C1~C5 는 @Builder/@Data 가 무엇을 생성하는지만 증명. Lombok 금지 자체는 OSS best practice 가 아님 — Buckpal (Hombergs 책 공식 예제, 2.5k stars) 이 domain 에 Lombok 을 명시 allowlist. ca-tmpl D3 는 bytecode 감각성 + framework 독립성을 위한 자체 stricter taste 결정. UNSUPPORTED_DECISION(CONTRARY) 라벨
D4 application-core -> adapter-* / app-bootstrap 의존 금지 raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C2, raw/company-tech-blogs/modulith-kakaobank-techblog-2025.md#KAKAOBANK-MOD-C4, raw/official-docs/arch-hexagonal-cockburn.md#HEX-COCKBURN-ORIG-C3 company-case-study + engineering-blog repository port가 아직 domain/repository에 남은 부분은 feature-application-port-usecase-contract와 동기화 필요
D5 adapter module 간 직접 의존 금지 raw/company-tech-blogs/modulith-kakaobank-techblog-2025.md#KAKAOBANK-MOD-C2, raw/company-tech-blogs/modulith-kakaobank-techblog-2025.md#KAKAOBANK-MOD-C4 company-case-study Spring Modulith 없이 public API/named interface 강제는 약함. ArchUnit/package-private convention 필요
D6 shared-contract business/domain concept 금지 raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011.md#SCREAM-C1, raw/branch-notes/feature-skeleton-package-blueprint-contract.md engineering-blog + project-decision shared module이 커질수록 common dumping ground가 될 위험
D7 sample-portfolio production 역수입 금지 raw/branch-notes/feature-skeleton-package-blueprint-contract.md project-decision 외부 직접 근거는 약함. Gradle dependency rule + ArchUnit failure로 실증 필요
D8 application @Transactional 직접 import 금지 raw/branch-notes/feature-application-port-usecase-contract.md, raw/official-docs/spring-tx-management-reference.md / CONTRARY EVIDENCE: raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data.md#SPRING-MOD-TX-C1 (Spring Modulith 공식 incubator 가 @ApplicationModuleListener@Transactional 을 meta-annotation 재노출 — Spring 팀 방향과 정면 충돌), raw/company-tech-blogs/buckpal-archunit-lombok-allowlist-direct-transactional.md#BUCKPAL-TX-C1 (hex-arch 공식 reference 가 @Component @Transactional 직접 부착) project-decision + official-vendor-doc + UNSUPPORTED_DECISION (CONTRARY) Spring 공식은 @Transactional 을 지원하고 Spring Modulith 는 meta-annotation 으로 재노출. Buckpal hex-arch reference 도 직접 부착. ca-tmpl D8 forbidden 정책은 OSS best practice 가 아님 — multi-Gradle-module Hexagonal context 에서 application 모듈 순수성을 위한 자체 stricter 결정. UNSUPPORTED_DECISION(CONTRARY) 라벨
D9 MapStruct generated exemption은 @Generated annotation 기반 ArchUnit predicate 로 허용 raw/official-docs/mapstruct-generated-annotation-official.md#MS-ANNOT-C1, #MS-ANNOT-C2 (MapStruct @Generated annotation 부착 공식 + suppressGeneratorTimestamp 옵션) + raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data.md#SPRING-MOD-AU-C1 (Spring Modulith 자체가 annotatedWith(Generated.class) predicate 를 production 코드에 사용 — DSL 패턴 OSS 검증) official-vendor-doc + company-case-study annotation FQN 주의: MapStruct = javax.annotation.processing.Generated, Spring AOT = org.springframework.aot.generate.Generated — ArchUnit predicate 는 MapStruct FQN 사용해야 함. 구현 권고: .and().areNotAnnotatedWith(javax.annotation.processing.Generated.class). ca-tmpl 실제 generated path 검증은 sample mapper 추가 후 통합 테스트 (needs-confirmation from plannedactually-implemented 승급 가능)
D10 ArchUnit rule은 JUnit 기반 test로 실행 raw/official-docs/archunit-user-guide.md#ARCHUNIT-UG-C6, raw/official-docs/archunit-user-guide.md#ARCHUNIT-UG-C5 official-vendor-doc FreezingArchRule baseline 정책은 별도 claim 보강 전까지 needs-confirmation으로 둠
D11 application-coreorg.springframework.context.ApplicationContext 자체에 의존 금지 (banned-class ArchUnit rule). class-literal 기반 getBean(Class<T>) 호출까지는 catch 가능 raw/official-docs/archunit-user-guide.md#ARCHUNIT-UG-C5 (JavaMethodCall / JavaConstructorCall bytecode 기반 access analysis) official-vendor-doc string-key bean lookup (getBean(String)) 과 Class.forName(String) 의 string target 은 ArchUnit 이 catch 불가 — D12 의 code review checklist 로 보완
D12 string-key bean lookup / Class.forName(String) / BeanFactory#getBeansOfType 의 reflection-style bypass 는 ArchUnit static analysis 의 한계 — code review checklist 항목으로 보완 raw/official-docs/archunit-user-guide.md (§6.2 "accesses ... bytecode offers all this information" — bytecode 가 string content 자체를 노출하지 않음) official-vendor-doc (negative claim — ArchUnit 이 못 잡는다는 사실의 공식 근거) runtime container 의존성 그래프 검증 도구 (Spring Boot Actuator /beans, Spring Modulith verifier) 도입은 후속 검토 — D1 Open Risk 구체화

검증해야 할 주장

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

Claim Why uncertain How to verify Status
domain-core가 Spring/JPA/HTTP/adapter/Lombok import를 포함하면 ArchUnit이 실패한다 rule DSL 구현 전에는 문서상 금지에 불과함 violating class 추가 → ./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' 실패 확인 + ArchitectureViolationFixtureTest#domain_is_pure_catches_spring_dependency_in_domain_package negative test 통과 확인 actually-implemented (2026-05-28 round 2) — Lombok forbidden 추가됨 (lombok.. package). violations-as-data fixture SpringDependentDomainFixturedomain_is_pure rule 의 실 catch 동작을 commit 된 negative test 로 보증.
application-coreadapter-* project dependency를 선언하면 Gradle 검증이 실패한다 Gradle dependency graph rule이 실제로 모든 subproject를 검사해야 함 application-coreimplementation project(':adapter-persistence') 추가 → ./gradlew verifyCleanArchitectureDependencies 실패 확인 actually-implemented
adapter module끼리 직접 의존하면 실패한다 adapter 간 공유 요구가 생길 때 우회 가능성이 있음 adapter-web -> adapter-persistence dependency 추가 → Gradle/ArchUnit 실패 확인 actually-implemented
shared-contract에 domain-specific package/class가 들어오면 실패한다 shared 허용 scope가 넓으면 domain concept가 흘러들 수 있음 shared-contract/src/main/java/.../shared/worklog/ 추가 → ArchUnit package scope rule 실패 확인 locally-verified
production module이 sample-portfolio에 의존하면 실패한다 sample은 편의상 import되기 쉬움 production module에 implementation project(':sample-portfolio') 추가 → Gradle dependency rule 실패 확인 locally-verified
controller가 domain object를 response로 반환하면 실패한다 mapper boundary rule이 module boundary만으로는 잡히지 않을 수 있음 adapter-web controller가 domain model을 직접 반환하도록 위반 코드 추가 → ArchUnit 실패 확인 locally-verified
application use case가 @Transactional을 직접 import하면 실패한다 Spring 공식은 @Transactional을 지원하므로 ca-tmpl 자체 결정임 violating use case 추가 → ArchUnit forbidden import 실패 확인 locally-verified
MapStruct generated mapper exemption이 의도한 package/path에만 적용된다 generated source path가 build tool 설정에 따라 달라질 수 있음 MapStruct sample mapper 추가 → generated path 확인 → exemption 외 경로 import 시 실패 확인 needs-confirmation
runtime lookup 우회는 ArchUnit으로 잡히지 않는다 ArchUnit은 bytecode/static dependency 중심이므로 false negative 가능 ApplicationContext#getBean 우회 코드 추가 → ArchUnit false-pass 확인 → manual/Sonar 보완 항목 등록 actually-implemented (2026-05-28 round 2) — D11 application_does_not_depend_on_application_context ArchUnit rule 작성. ApplicationContextDependentFixture negative test 가 catch 동작 commit 보증. getBean(String) string-key bypass 와 Class.forName(String) 은 여전히 ArchUnit 정적 분석 범위 밖 (D12) — application-core/CLAUDE.md forbidden 섹션에 명시.
ca-tmpl repo-local workflow docs가 non-trivial 구현 후 LLM Wiki branch-note와 derived raw notes 캡처를 요구한다 문서 규칙 반영만으로 실제 에이전트 실행을 자동 보장하지는 않음 AGENTS.md, CLAUDE.md, .agents/.claude/.codex 지침에서 llm-wiki-capture 및 Wiki capture 문구 검색 documented-only
ArchUnit rule 이 위반을 실제로 catch 한다는 commit 된 증명 (negative test fixture) — violations-as-data pattern 채택 가능 현재 negative 검증은 임시 violating 파일 추가 → 제거 방식 (regression 보호 없음). Spring Modulith 공식 패턴 modules.detectViolations().getMessages() + example/ninvalid fixture package 가 production precedent src/app-bootstrap/src/test/.../violations/ 패키지에 의도된 위반 fixture class 추가 + assertThatThrownBy(rule::check) 또는 evaluationResult.getFailureReport().getDetails() assertion 으로 exact match 검증. 근거: raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data.md#SPRING-MOD-AU-C2 actually-implemented (2026-05-28 round 2) — src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/violations/ 에 6 fixture (1 domain + 5 application) + ArchitectureViolationFixtureTest 에 6 negative test 작성. 각 test 가 ClassFileImporter().importPackages("...violations") 로 fixture 만 로드한 뒤 해당 rule 의 EvaluationResult.hasViolation() == true assert. fixture 는 src/test/... 위치라 main @AnalyzeClasses(importOptions = DoNotIncludeTests.class) 가 제외 → main suite vacuous pass 위험 없음. Spring Modulith example/ninvalid 패턴의 ca-tmpl 채택.

테스트 계약

  • domain-core가 Spring / JPA / HTTP DTO / adapter module type을 참조하면 실패.
  • application-coreadapter-web, adapter-persistence, adapter-outbound, app-bootstrap에 의존하면 실패.
  • adapter module끼리 직접 의존하면 실패.
  • production module이 sample-portfolio을 import하거나 dependency로 선언하면 실패.
  • shared-contract에 business/domain package 또는 domain-specific class가 추가되면 실패.
  • adapter-web controller가 domain object를 response로 직접 반환하면 실패.
  • application-coreorg.springframework.transaction.annotation.Transactional을 직접 import하면 실패.
  • MapStruct generated exemption 밖의 generated code 우회가 있으면 실패.
  • application-coreorg.springframework.context.ApplicationContext 를 직접 의존하면 실패 (D11 banned-class rule). getBean(Class) class-literal 호출도 이 rule 로 catch.
  • domain-core 가 Lombok generated bytecode 를 포함하면 실패 (@Builder / @Data / @Getter / @Setter 등 Lombok annotation 사용 금지 — feature-skeleton-package-blueprint-contract Option A 채택).

구현 가이드

본 branch 의 결정 → 구현 위치 명세. 각 row 는 본 branch 의 Decision ID + Supporting Claim reference 를 가진다(CLAUDE.md §15.5 R1). 근거가 원칙 만 권고하고 detail 은 구현자 trade-off 인 cell 은 UNSUPPORTED_IMPL_DECISION 라벨을 단다(R2). 본 branch 범위 밖 detail 은 남기지 않는다(R3).

⚠️ 이 명세는 사후 정제 다 — 본 branch 는 2026-05-28 시점에 이미 구현·검증 완료(§진행 중 메모)되었고, 본 section 은 ground-truth(@db61075) 와 대조해 실제 구현 위치를 역으로 명세화한 것이다.

Decision 구현 위치 (ground-truth @db61075) 메커니즘 detail Trace
D1 (test 강제) app-bootstrap/.../architecture/CleanArchitectureTest.java @AnalyzeClasses(packages = "dev.caskeleton", importOptions = DoNotIncludeTests.class) + @ArchTest static final ArchRule 필드들. @AnalyzeClasses import scope 선택은 UNSUPPORTED_IMPL_DECISION — ARCHUNIT-UG-C6 는 JUnit 통합만 권고하고 base package 선택은 프로젝트 trade-off (dev.caskeleton root 단일 scan 으로 결정) D1 / ARCHUNIT-UG-C5,C6
D2 (build-graph) src/build.gradle:53 verifyCleanArchitectureDependencies task allowedProjectDependencies Map<String,Set> 화이트리스트 9 module + configurations(api/implementation/compileOnly/runtimeOnly).dependencies.withType(ProjectDependency) 차집합 → GradleException. matrix 구체 값은 UNSUPPORTED_IMPL_DECISION — blueprint 결정(raw/branch-notes/feature-skeleton-package-blueprint-contract)의 module 목록에서 도출한 trade-off D2 / WW-HEX-C1, KAKAOBANK-MOD-C4
D3 (domain purity + Lombok) domain_is_pure rule noClasses().that().resideInAPackage("..domain..").should().dependOnClassesThat().resideInAnyPackage(..., "lombok..", ...) + .allowEmptyShould(true). forbidden package 목록 구체값은 UNSUPPORTED_IMPL_DECISION (CONTRARY: Buckpal 은 lombok.. allowlist — D3 Open Risk) D3 / LMB-C1~C5, ARCHUNIT-UG-C4
D4 (application 격리) application_does_not_depend_on_adapters_or_transport rule noClasses().that().resideInAPackage("..application..").should().dependOnClassesThat().resideInAnyPackage("..adapter..","..bootstrap..","org.springframework.web..",...) D4 / WW-HEX-C2, HEX-COCKBURN-ORIG-C3
D5 (adapter-adapter 격리) web_/persistence_/outbound_adapter_does_not_depend_on_* 3 rule 각 adapter package 가 sibling adapter package 에 의존 금지. ArchUnit package glob 으로 구현 (Spring Modulith named interface 미사용 — D5 Open Risk) D5 / KAKAOBANK-MOD-C2,C4
D6 (shared scope) shared_contract_contains_only_operational_contract_packages rule classes().that().resideInAPackage("..shared..").should().resideInAnyPackage(<operational allowlist>). allowlist package 집합은 UNSUPPORTED_IMPL_DECISION — blueprint 의 operational contract 목록에서 도출 D6 / SCREAM-C1
D7 (sample 역수입 금지) production_code_does_not_depend_on_sample_portfolio rule noClasses().that().resideOutsideOfPackage("..sample.portfolio..").should().dependOnClassesThat().resideInAPackage("..sample.portfolio..") D7 (project-decision)
D11 (ApplicationContext banned-class) application_does_not_depend_on_application_context rule noClasses().that().resideInAPackage("..application..").should().dependOnClassesThat().haveFullyQualifiedName("org.springframework.context.ApplicationContext"). FQN 단일 class 선택은 bytecode access analysis(ARCHUNIT-UG-C5)로 getBean(Class) 까지만 catch D11 / ARCHUNIT-UG-C5
S1 (violations-as-data) ArchitectureViolationFixtureTest + architecture/violations/ package 본 branch fixture: SpringDependentDomainFixture(D3), ApplicationContextDependentFixture(D11), TransactionalAnnotatedFixture. 각 test 가 rule.evaluate(VIOLATION_CLASSES).hasViolation()==true assert. fixture 가 src/test/ 위치라 main DoNotIncludeTests 분석 제외 → vacuous pass 방지 S1 / SPRING-MOD-AU-C2

OUT_OF_BRANCH_SCOPE (R3): ground-truth CleanArchitectureTest 의 boundary-validation(B1/B2/B4/B5/B6/B7, D5 ProblemDetail), streaming(D3 SSE/WebSocket), serialization(BigDecimal), resource-identifier(D17 no_long_id_pk 등), api-contract(D19 AIP-122), business-rule-validation(C1/D1 jakarta.validation) rule 들은 각각 다른 branch 소유다. 본 §에는 남기지 않으며 해당 branch ingest 에서 명세한다. @Transactional ban rule(application_does_not_use_spring_transactional_annotation)은 코드 attribution 상 raw/branch-notes/feature-application-port-usecase-contract D3 소유지만 본 branch 테스트 계약에도 포함되어 red/green 확인됨 — SSOT 는 그 branch.

엣지·실패·의존

본 branch 구현의 경계 조건 · 알려진 실패 모드 · 외부 의존. ArchUnit 정적 분석의 한계를 정직하게 남긴다.

  • Edge — empty anchor: skeleton 단계의 빈 module 은 that() 매칭 대상이 0개라 ArchUnit 기본 동작상 failed to check any classes 로 실패한다. 의도된 빈 anchor rule 에 allowEmptyShould(true) 를 명시해 허용. 근거 사실: raw/errors/archunit-empty-should-anchor-2026-05-27.
  • Failure — vacuous pass (import scope 누락): 검사 대상 class 가 @AnalyzeClasses import scope 밖이면 위반이 있어도 rule 이 조용히 통과(BUILD SUCCESSFUL, 에러 신호 없음)한다. allowEmptyShould(true) 도 이 false-negative 를 막지 못함. 보완: sample/fixture 를 test import scope 에 포함 + violations-as-data negative fixture(S1) 로 catch 동작을 commit 보증. 근거 사실: raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28.
  • Failure — D11/D12 string-key bypass (알려진 한계, 보완 불가): D11 banned-class rule 은 class-literal getBean(Class<T>) 까지만 catch 한다. string-key getBean(String) · Class.forName(String) · BeanFactory#getBeansOfType 같은 reflection-style bypass 는 bytecode 가 문자열 내용을 노출하지 않아 ArchUnit 정적 분석으로 잡을 수 없다(D12). application-core/CLAUDE.md forbidden 섹션의 code review checklist 로만 보완 — 자동 강제 장치 아님. runtime 검증(Actuator /beans, Modulith verifier)은 후속 후보(D1/D12 Open Risk).
  • Dependency — testCompileOnly: TransactionalAnnotatedFixture@Transactional 을 import 하기 위해 app-bootstrap/build.gradletestCompileOnly 'org.springframework:spring-tx' 추가(production 영향 없음). 관련 class-loading 이슈: raw/errors/archunit-testcompileonly-class-loading-2026-06-02.
  • Dependency — sandbox/Gradle: Gradle wrapper 가 sandbox 기본 권한에서 ~/.gradle lock 파일 생성 실패 → escalated 실행으로 해결. 근거: raw/errors/gradle-wrapper-readonly-cache-2026-05-28.
  • Edge — MapStruct exemption (미검증): D9 generated mapper @Generated exemption 은 ca-tmpl 에 실제 MapStruct mapper 가 아직 없어 needs-confirmation — sample mapper 추가 후 통합 검증 필요.

마주친 문제

  • 2026-05-28: Gradle wrapper sandbox 권한 문제

    • 원인: sandbox 기본 권한 정책 상 ~/.gradle 디렉터리 쓰기가 차단되어 wrapper 가 lock/cache 파일 생성 실패.
    • 시도: 기본 권한으로 ./gradlew :app-bootstrap:test 실행 → Read-only file system 오류.
    • 해결: 사용자 승인된 escalated 실행으로 동일 명령 재수행 → 성공. 검증 결과는 본 branch-note 진행 중 메모 2026-05-28 항목 참조.
    • 별도 에러 노트: raw/errors/gradle-wrapper-readonly-cache-2026-05-28
  • 2026-05-28: 계획 문서가 .gitignore/docs 규칙에 가려져 git untracked

    • 원인: ca-tmpl .gitignore/docs 디렉터리를 전면 제외 (operational docs 는 별도 repo 분리 정책).
    • 시도: docs/superpowers/plans/2026-05-28-architecture-enforcement-rules.md 작성 → git status 에 미포함 확인.
    • 해결: 계획 문서는 작업용으로만 유지하고 최종 기록은 본 branch-note 의 진행 중 메모 / 결정 사항 / Closure 섹션에 통합. 계획 문서 위치는 untracked 로 두되 본 메모에서만 참조.
    • 별도 에러 노트: (해당 없음 — 운영 메모, 재발 시 동일 정책 적용)
  • 2026-05-28: repo-local workflow 문서 패치 중 자동 승인 검토 차단

    • 원인: .agents/.claude/.codex 프롬프트 반영 패치 도중 도구의 automatic approval review가 patch 적용을 차단.
    • 시도: 먼저 AGENTS.md, CLAUDE.md, llm-wiki-capture.md까지 반영한 뒤 남은 plugin/agent prompt 반영을 진행하려 했으나 중단.
    • 해결: 사용자에게 차단 상태와 부분 반영 범위를 보고하고 명시 승인을 받은 뒤 남은 파일을 계속 반영.
    • 별도 에러 노트: raw/errors/apply-patch-auto-approval-rejected-2026-05-28
  • 2026-06-30: develop 머지 과정에서의 매퍼 아키텍처 규칙 오탐지

    • 원인: ..mapper.. 패키지 규칙이 영속성 매퍼뿐만 아니라 웹 매퍼까지 과도하게 필터링하여 웹/애플리케이션 레이어 의존성을 차단함.
    • 해결: 영속성 매퍼(..adapter.persistence..mapper..)로 대상을 좁히고, Clean Architecture 의존성 방향(영속성 -> 애플리케이션 허용)에 맞춰 금지 목록에서 ..application..을 제외함.

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

이 섹션은 이 branch-note에서 실제로 파생된 raw/wiki 문서가 생겼을 때 링크한다. 구현 결과와 검증 증거는 TODO, 진행 중 메모, Claims To Verify, Closure에 기록한다.

근거 자료

Sub-branches (세부 작업)

  • (없음 — 이 branch는 별도 sub-branch 없이 ca-tmpl 코드 변경 2개 파일로 진행)

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

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

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

  • (없음 — 이번 구현 중 새 lecture note 생성 없음)

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

관련 일일 노트

완료 후 정리

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

  • PR 링크: (미생성 — local branch feature/architecture-enforcement-rules, not merged)
  • 리뷰 메모: 2026-05-28 local branch feature/architecture-enforcement-rules에서 Gradle dependency verifier와 ArchUnit rule을 구현/보강했다. 구현 파일은 ca-tmpl src/build.gradle, src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java.
  • 머지 결과 / 배포 환경: not merged. local verification only.
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목:
      • Gradle verifyCleanArchitectureDependencies가 모든 declared module이 정책에 포함되는지 검사하고, 허용되지 않은 project dependency를 실패 처리한다.
      • CleanArchitectureTest가 domain purity (Spring/JPA/Hibernate/Lombok 모두 forbidden), application -> adapter/bootstrap 금지, adapter 간 직접 의존 금지, web DTO boundary, production -> sample-portfolio dependency 금지, application @Transactional 금지, application ApplicationContext 금지 (D11), inbound use-case naming + capability mandatory + KEYED idempotency freeze (D14) 를 검사한다.
      • ArchitectureViolationFixtureTest 가 위 rule 6종의 실 catch 동작을 violations-as-data fixture 로 보증한다 (src/app-bootstrap/src/test/java/.../violations/).
    • locally-verified 항목:
      • 임시 shared.worklog package 추가 시 shared-contract package allowlist rule이 실패함을 확인했다.
      • 임시 controller가 production domain object를 직접 반환할 때 ArchUnit rule이 실패함을 확인했다.
      • 임시 mapper가 application boundary에 의존할 때 mapper boundary rule이 실패함을 확인했다.
      • 임시 application class가 Spring @Transactional을 import할 때 ArchUnit rule이 실패함을 확인했다.
      • 임시 app-bootstrap -> sample-portfolio project dependency 선언 시 verifyCleanArchitectureDependencies가 실패함을 확인했다.
      • cd src && ./gradlew :app-bootstrap:test verifyCleanArchitectureDependencies 성공.
      • cd src && ./gradlew test 성공.
    • documented-only 항목:
      • ca-tmpl repo-local 문서에 non-trivial 구현 후 LLM Wiki branch-note와 derived raw notes 캡처를 수행하도록 documented-only workflow rule을 추가했다.
    • prod-verified 항목:
      • (없음)
  • 추출하지 않을 항목 (planned / documented-only / abandoned):
    • MapStruct generated mapper exemption은 아직 needs-confirmation이다.
    • runtime lookup / reflection 우회 false-pass 확인은 아직 planned이다.
    • Spring Modulith verifier 도입은 out of scope 후속 후보로 유지한다.
    • SonarQube custom rule 구현과 CI workflow job 분리는 out of scope다.