Files
llm-wiki/vault/10-projects/ca-skeleton-operational-contract/branch-notes/feature-architecture-enforcement-rules.md
T

393 lines
50 KiB
Markdown

---
title: branch / feature-architecture-enforcement-rules
source_type: branch-note
status: verified
branch: feature-architecture-enforcement-rules
parent_branch:
related_projects: [ca-skeleton]
tags: [branch, ca-skeleton, architecture, enforcement, archunit, clean-architecture]
created: 2026-05-21
last_reviewed: 2026-06-04
target_merge: master
status_label: review
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-018
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-018
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-ARCHTEST-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
contract_packet_sha256: a1912f62e082b02487a0a332eef101b12c056ac485ec9d1bbb42e4e1590ecb05
---
> **Ground-truth 대조 (2026-06-04, ca-tmpl `@db61075`)**: 본 branch가 정의한 enforcement rule이 실제 레포에 반영됨을 확인. `app-bootstrap/.../architecture/CleanArchitectureTest.java`에 `domain_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로 강제하는 기준을 정의합니다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 §20 Skeleton Blueprint Contract 와 §25 Critical Defaults 의 architecture enforcement 영역을 정제한다.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: forbidden module/import fixture가 ArchUnit gate에서 실패한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| 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]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
문서 기준만으로는 시간이 지나면 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)
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- 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`
- [x] Gradle project dependency rule을 `domain-core`, `application-core`, `adapter-*`, `app-bootstrap`, `sample-portfolio` 기준으로 정리 — 등급: `locally-verified`
- [x] ArchUnit `domain-core` forbidden import rule 정의 — 등급: `actually-implemented`
- [x] ArchUnit `application-core` adapter dependency forbidden rule 정의 — 등급: `actually-implemented`
- [x] adapter module 간 직접 의존 금지 rule 정의 — 등급: `actually-implemented`
- [x] `shared-contract` 허용 package scope rule 정의 — 등급: `locally-verified`
- [x] `sample-portfolio` production 역수입 금지 rule 정의 — 등급: `locally-verified`
- [x] mapper boundary / direct domain response 금지 rule 정의 — 등급: `locally-verified`
- [x] transaction annotation forbidden import rule 정의 — 등급: `locally-verified`
## 진행 중 메모
- architecture test 기본 도구는 ArchUnit으로 둔다.
- Gradle dependency graph 검증은 `feature-skeleton-package-blueprint-contract``verifyCleanArchitectureDependencies`와 같은 방향으로 둔다.
- Spring Modulith verifier는 기본값이 아니라 후속 검토 후보로 둔다. 현재 기본 강제선은 Gradle dependency rule + ArchUnit rule이다.
- 2026-05-28 구현 반영: ca-tmpl `src/build.gradle``verifyCleanArchitectureDependencies`를 보강하고, `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-core``adapter-*``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 `planned``actually-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-core``org.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 `SpringDependentDomainFixture``domain_is_pure` rule 의 실 catch 동작을 commit 된 negative test 로 보증. |
| `application-core``adapter-*` project dependency를 선언하면 Gradle 검증이 실패한다 | Gradle dependency graph rule이 실제로 모든 subproject를 검사해야 함 | `application-core``implementation 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-core``adapter-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-core``org.springframework.transaction.annotation.Transactional`을 직접 import하면 실패.
- MapStruct generated exemption 밖의 generated code 우회가 있으면 실패.
- `application-core``org.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<String>> 화이트리스트 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.gradle``testCompileOnly '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에서 파생된 자료)
<!-- GENERATED: sources:start -->
- [[raw/company-tech-blogs/buckpal-archunit-lombok-allowlist-direct-transactional]]
- [[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/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data]]
- [[raw/official-docs/adapter-spring-boot-autoconfig-custom-starter]]
- [[raw/official-docs/arch-clean-architecture-uncle-bob]]
- [[raw/official-docs/arch-hexagonal-cockburn]]
- [[raw/official-docs/archunit-user-guide]]
- [[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/lombok-builder-data-features-official]]
- [[raw/official-docs/mapstruct-generated-annotation-official]]
- [[raw/official-docs/modulith-spring-official-doc]]
- [[raw/official-docs/onion-palermo-original-2008]]
- [[raw/official-docs/persistence-osiv-antipattern-hibernate-vladmihalcea]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: interviews:start -->
- [[raw/interviews/archunit-manual-importer-vs-analyzeclasses]]
- [[raw/interviews/archunit-static-analysis-limits]]
- [[raw/interviews/clean-architecture-boundary-enforcement]]
- [[raw/interviews/post-implementation-knowledge-capture]]
<!-- GENERATED: interviews:end -->
<!-- GENERATED: errors:start -->
- [[raw/errors/apply-patch-auto-approval-rejected-2026-05-28]]
- [[raw/errors/gradle-wrapper-readonly-cache-2026-05-28]]
<!-- GENERATED: errors:end -->
<!-- GENERATED: daily-notes:start -->
- [[raw/daily-notes/2026-05-28]]
<!-- GENERATED: daily-notes:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28]]
- [[raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28]]
- [[raw/blog-topics/post-implementation-knowledge-capture-workflow-2026-05-28]]
<!-- GENERATED: blog-topics:end -->
> 이 섹션은 이 branch-note에서 실제로 파생된 raw/wiki 문서가 생겼을 때 링크한다. 구현 결과와 검증 증거는 `TODO`, `진행 중 메모`, `Claims To Verify`, `Closure`에 기록한다.
### 근거 자료
- [[raw/official-docs/mapstruct-generated-annotation-official]] — D9: MapStruct `@Generated` annotation 공식 근거 (MS-ANNOT-C1, MS-ANNOT-C2)
- [[raw/official-docs/lombok-builder-data-features-official]] — D3: `domain-core` Lombok 금지 결정 공식 근거 (`@Builder` 7가지 생성 요소 + `@Data` setter 생성 범위, LMB-C1~LMB-C5)
- [[raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data]] — D9 corroborate: Spring Modulith 자체가 `annotatedWith(Generated.class)` predicate 를 production 에서 사용 (SPRING-MOD-AU-C1). S1 negative test fixture: `detectViolations()` violations-as-data 패턴 (SPRING-MOD-AU-C2)
- [[raw/company-tech-blogs/buckpal-archunit-lombok-allowlist-direct-transactional]] — D3 CONTRARY evidence: Buckpal domain purity ArchUnit rule 이 `lombok..` 를 명시적 allowlist 함 (BUCKPAL-LOMBOK-C1, BUCKPAL-LOMBOK-C2). ca-tmpl D3 가 OSS 다수파가 아닌 stricter stance 임을 뒷받침
### Sub-branches (세부 작업)
- (없음 — 이 branch는 별도 sub-branch 없이 ca-tmpl 코드 변경 2개 파일로 진행)
### 오류 기록 (이 branch 작업 중 발생)
- [[raw/errors/gradle-wrapper-readonly-cache-2026-05-28]] — Gradle wrapper가 sandbox 기본 권한에서 `~/.gradle` lock 파일을 만들지 못한 문제
- [[raw/errors/apply-patch-auto-approval-rejected-2026-05-28]] — repo-local workflow 문서 패치 중 자동 승인 검토가 차단되어 사용자 명시 승인 후 재개한 문제
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- [[raw/interviews/clean-architecture-boundary-enforcement]] — Clean Architecture 경계를 Gradle/ArchUnit으로 자동 검증한 경험에서 파생된 예상 질문
- [[raw/interviews/post-implementation-knowledge-capture]] — 구현 완료 후 branch-note와 파생 raw 문서를 어떻게 남길지에 대한 예상 질문
- [[raw/interviews/archunit-static-analysis-limits]] — ArchUnit static analysis 의 한계 (string-key bypass / vacuous pass / generated code) 와 violations-as-data 보완 패턴 (round 2 D11/D12 + Claims to Verify).
### 강의 (이 작업을 위해 학습한 강의)
- (없음 — 이번 구현 중 새 lecture note 생성 없음)
### job-posting tie-ins (이 작업에서 파생된 글감)
- [[raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28]] — Clean Architecture 경계를 Gradle/ArchUnit rule로 자동 검증한 경험에서 파생된 블로그 글감
- [[raw/blog-topics/post-implementation-knowledge-capture-workflow-2026-05-28]] — 구현 완료 후 지식 캡처를 repo-local workflow로 강제하는 설계 글감
- [[raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28]] — Spring Modulith 의 `example/ninvalid` 패턴을 차용해 6 fixture + 6 negative test 로 ArchUnit rule 의 실 catch 동작을 commit 보증한 작업 글감 (round 2).
- (job-posting 없음 — 이번 작업에는 연결할 실제 채용공고 원문/URL이 없음)
## 관련 일일 노트
- [[raw/daily-notes/2026-05-27]]
- [[raw/daily-notes/2026-05-28]]
## 완료 후 정리
> 머지/종료 시점에 채움. `/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다.