Files
llm-wiki/raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28.md
T

122 lines
14 KiB
Markdown

---
title: blog-topic / clean-architecture-boundary-enforcement-2026-05-28
source_type: blog-topic
status: raw
related_branches: [feature-architecture-enforcement-rules]
related_projects: [ca-tmpl]
tags: [blog-topic, ca-tmpl, architecture, testing, archunit, clean-architecture, gradle]
created: 2026-05-28
status_label: ready-for-canonical
target_audience: backend-engineer
inspiration_url:
archive_url:
---
# blog-topic: clean-architecture-boundary-enforcement-2026-05-28
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
## Parent / 부모
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — Clean Architecture 경계를 Gradle / ArchUnit fitness function 으로 실 강제한 구현·검증 경험 (Decisions D1~D10 + Claims to Verify 표).
- [[raw/project-notes/ca-skeleton-operational-contract]] — ca-tmpl skeleton 의 module boundary / operational contract SSOT.
## 트리거 / Trigger
- 트리거 유형: `branch-work`
- 트리거 날짜: 2026-05-28
- 트리거 연결 노트: [[raw/branch-notes/feature-architecture-enforcement-rules]] — `Decision D1` (CA 경계는 architecture test 로 강제) 을 실제 코드와 테스트로 붙인 작업.
## 글감 / Topic seed
- 한 문장 요지: Clean Architecture 는 문서로만 선언하면 시간이 지나면 무너지므로, **Gradle module dependency rule (1차) + ArchUnit bytecode fitness function (2차)** 으로 _역할을 나눠_ 자동 검증해야 한다. 한쪽만으로는 항상 false-pass 가 남는다.
- 떠오른 계기: `feature-architecture-enforcement-rules` 작업에서 ca-tmpl `src/build.gradle``verifyCleanArchitectureDependencies``src/app-bootstrap/src/test/.../CleanArchitectureTest.java` 를 함께 보강하고, 임시 위반 코드로 red/green 검증까지 마침.
- 예상 제목 후보:
- Clean Architecture 경계를 _문서가 아니라 테스트_ 로 지키기 — Gradle + ArchUnit 의 분업
- Gradle 이 잡는 것 vs ArchUnit 이 잡는 것 — module graph 와 bytecode rule 의 역할 분리
- 빈 anchor module 도 ArchUnit 으로 검증할 수 있을까? — `allowEmptyShould(true)` 의 정직한 사용
## 핵심 주장 후보 / Claim candidates
> 아직 canonical 이 아니다. 사실/경험/의견 후보를 분리한다. 각 항목은 branch-note Decision ID 또는 외부 source claim ID 로 근거를 인용한다.
- 사실 후보:
- ca-tmpl 의 boundary 강제는 **Gradle 의 project-dependency 매트릭스 + ArchUnit fitness function** 두 층으로 구성된다. Gradle 은 _build graph 수준_, ArchUnit 은 _bytecode/import 수준_ 을 막는다. 둘은 잡는 위반의 종류가 다르다 — 근거: `feature-architecture-enforcement-rules.md` D1 (CA 경계 = architecture test 강제), D2 (package rule = Gradle multi-module boundary). 외부 근거: `raw/official-docs/governance-archunit-official.md#AU-OFF-C1`, `raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C5`.
- `domain-core``org.springframework..`, `jakarta.persistence..`, `..adapter..`, `..application..`, `..bootstrap..` import 모두 금지 — 근거: `feature-architecture-enforcement-rules.md` D3 (domain-core forbidden import). 외부 근거: `raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C1` (Domain / Application / Framework / Bootstrap 4-module 격리 사례).
- `application-core``org.springframework.transaction.annotation.Transactional`_직접 import_ 하면 ArchUnit rule 이 실패. Spring 공식은 `@Transactional` 직접 부착을 권고 (다수파) 이므로 ca-tmpl 은 의도적 소수파 결정 — 근거: `feature-architecture-enforcement-rules.md` D8 (application `@Transactional` 직접 import 금지) + `feature-application-port-usecase-contract.md` D3 (TransactionPort abstraction). 외부 근거: `raw/official-docs/at-transactional-spring-official.md#AT-TX-C1` (Spring 공식 권고).
- `production_code_does_not_depend_on_sample_ticket` ArchUnit rule + Gradle `verifyCleanArchitectureDependencies` 가 동시에 sample-ticket 역수입을 차단 — 근거: `feature-architecture-enforcement-rules.md` D7 (sample-ticket production 역수입 금지) + Claims to Verify "production module 이 `sample-ticket` 에 의존하면 실패한다" → `locally-verified`.
- 경험 후보:
- 임시 위반 코드 (`shared.ticket` package, controller 의 domain return, application 의 `@Transactional`, `app-bootstrap -> sample-ticket` Gradle dep) 를 각각 추가해 신규 rule 이 실패함을 red/green 으로 확인 → 위반 제거 후 `cd src && ./gradlew :app-bootstrap:test verifyCleanArchitectureDependencies``cd src && ./gradlew test` 모두 통과 — 근거: `feature-architecture-enforcement-rules.md` §진행 중 메모 2026-05-28 항목 + Closure §`locally-verified`.
- 빈 skeleton anchor module 이 ArchUnit empty-should failure 를 일으켜 `allowEmptyShould(true)`_빈 상태가 의도된 rule 에만_ 선별 적용 — 근거: `feature-architecture-enforcement-rules.md` §마주친 문제 + 파생 에러 [[raw/errors/archunit-empty-should-anchor-2026-05-27]].
- Codex sandbox 의 read-only `~/.gradle` 권한 때문에 wrapper 가 lock 파일을 못 만들어 실행이 실패 → 사용자 승인 escalation 으로 재실행 — 근거: 파생 에러 [[raw/errors/gradle-wrapper-readonly-cache-2026-05-28]].
- 의견 / 해석 후보:
- Gradle 의 project dependency 매트릭스만으로는 _세부 import_ (e.g., controller 가 JPA entity 를 return type 으로 노출) 를 못 잡는다. ArchUnit 이 이걸 보완. 반대로 ArchUnit 만으로는 _module 간 build-graph 사이클_ 을 깔끔하게 못 잡는다. **둘을 분업하는 게 작은 skeleton 에서는 Spring Modulith 도입보다 가볍다** — 근거: `feature-architecture-enforcement-rules.md` §결정 사항 2026-05-27 "Spring Modulith verifier 도입은 out of scope §범위" + D5 Open Risk ("Spring Modulith 없이 public API 강제는 약함").
- **ArchUnit 은 reflection / runtime lookup 우회를 잡지 못한다** (`ApplicationContext#getBean` 류). 이건 ArchUnit 의 한계로 솔직히 인정해야 하며, Sonar custom rule 또는 review checklist 로 보완 — 근거: `feature-architecture-enforcement-rules.md` Claims to Verify "runtime lookup 우회는 ArchUnit 으로 잡히지 않는다" → `planned`.
- **다수파 (`@Transactional` 직접 부착) 도 합리적이다**. ca-tmpl 의 boundary 강제는 _template repository 라서_ 의도적으로 엄격한 소수파. 단일 DB / 단일 transaction manager 상황의 작은 팀은 다수파가 boilerplate 비용 측면에서 낫다 — 근거: `feature-architecture-enforcement-rules.md` D8 Open Risk + `feature-application-port-usecase-contract.md` 외부 근거 §대안 비교.
## Outline seed
> 각 섹션 옆에 `→ 핵심 메시지` 를 함께 명시한다.
1. 동기 — Clean Architecture 를 "했다" 고 말하지만 controller 가 repository 를 import 해도 빌드가 도는 흔한 상황 → **문서만으로는 boundary drift 가 누적된다.**
2. 두 층의 분업 — Gradle project-dependency matrix vs ArchUnit bytecode rule → **각자 잡는 위반 종류가 다르고 한쪽만으로는 false-pass 가 남는다.**
3. 실제 구현 스케치 — `verifyCleanArchitectureDependencies` 의 allowed map + `CleanArchitectureTest` 의 12개 rule → **rule 은 _도메인 추가_ 보다 _도메인 누락_ 으로 더 자주 깨진다 (예: 새 module 추가 시 양쪽 다 업데이트 필요).**
4. red/green 으로 rule 을 _믿을 수 있게_ 만들기 — 임시 위반 코드 4종 추가 → 실패 확인 → 제거 → 통과 → **rule 이 _진짜로 잡는지_ 를 매번 검증하지 않으면 silent regression 이 생긴다.**
5. 빈 anchor 문제 — skeleton template 에서 production 코드가 비어 있는 상태도 valid → **`allowEmptyShould(true)`_빈 상태가 의도된 rule_ 에만 선별 적용. 모든 rule 에 일괄 적용하면 안 됨.**
6. ArchUnit 의 한계와 보완 — runtime reflection / MapStruct generated path / Spring Modulith → **솔직한 한계 인정 + 보완 도구 (Sonar / review checklist / Modulith) 의 분업.**
7. template repository 라서 가능한 엄격함 — production project 와의 trade-off → **boundary 비용을 _learning cost_ 로 흡수할 수 있는 환경에서 강제하라.**
## Canonical 전환 후보 / Canonical extraction candidates
> `wiki/blog/` 로 바로 가지 않는다. 먼저 어떤 canonical 문서로 정제할지 기록한다.
- `wiki/projects/ca-tmpl/architecture-enforcement-rules.md` 후보:
- 실제 적용된 Gradle dependency matrix (모듈별 allowed list).
- 실제 작성된 ArchUnit rule 12종 (이름 + 잡는 위반).
- red/green 검증 절차 (`feature-architecture-enforcement-rules.md` Closure 의 `locally-verified` 5개 항목).
- `wiki/concepts/architecture-enforcement-testing.md` 후보:
- Gradle build-graph rule 과 ArchUnit bytecode rule 의 _역할 분리_ 패턴 (project-agnostic).
- `allowEmptyShould` 와 skeleton template 의 빈 anchor 처리 패턴.
- "ArchUnit 의 한계: runtime reflection / generated code" 일반 원칙.
- 필요한 추가 검증:
- runtime lookup / reflection 우회가 현재 rule 을 실제로 false-pass 하는지 PoC 실험 (`feature-architecture-enforcement-rules.md` Claims to Verify 의 `planned` 항목).
- MapStruct generated mapper exemption 경로 확인 (동일 표의 `needs-confirmation` 항목).
- Spring Modulith 를 도입했을 때 ArchUnit rule 과의 중복/대체 관계.
## Sources / 근거 후보
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — 구현 결정 (D1~D10), 검증 결과, Claims to Verify 의 status grading, Closure 의 `locally-verified` / `documented-only` 분리.
- [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] — module boundary 의 자매 결정 (D1~D8). 본 글의 Gradle dependency matrix 항목은 두 branch 결정의 교집합.
- [[raw/branch-notes/feature-application-port-usecase-contract]] — application 의 `@Transactional` 직접 import 금지 결정 (D3) 와 TransactionPort 추상화. 본 글의 다수파 vs 소수파 trade-off 단락 근거.
- [[raw/errors/archunit-empty-should-anchor-2026-05-27]] — 빈 anchor 와 `allowEmptyShould(true)` 의 선별 적용 사례.
- [[raw/errors/gradle-wrapper-readonly-cache-2026-05-28]] — sandbox 환경에서 build tool 실행 검증의 함정.
- [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] — `production_code_does_not_depend_on_sample_ticket` rule 의 test-scope inclusion 미묘함.
- [[raw/official-docs/archunit-user-guide]] — ArchUnit rule DSL / JUnit 통합 (`ARCHUNIT-UG-C5`, `ARCHUNIT-UG-C6`).
- [[raw/official-docs/governance-archunit-official]] — architecture test 로 governance 강제하는 일반 근거 (`AU-OFF-C1`, `AU-OFF-C2`).
- [[raw/official-docs/archunit-conditional-on-property-3-layer-pattern]] — predicate/condition 기반 fitness function 의 bytecode 모델 (`AUCP-C1` ~ `AUCP-C5`).
- [[raw/company-tech-blogs/woowahan-hexagonal-multimodule]] — Domain / Application / Framework / Bootstrap 4-module 분리 사례 (`WW-HEX-C1` ~ `WW-HEX-C5`).
- [[raw/company-tech-blogs/modulith-kakaobank-techblog-2025]] — Gradle multi-module + Hexagonal + Spring Modulith 사례 (`KAKAOBANK-MOD-C2`, `KAKAOBANK-MOD-C4`).
- [[raw/interviews/clean-architecture-boundary-enforcement]] — 같은 경험에서 파생된 예상 면접 질문.
## 미해결 / Unknown
- 아직 확인해야 할 사실: runtime lookup / reflection 우회가 현재 ArchUnit rule 을 실제로 false-pass 하는지 실험 미수행 (`feature-architecture-enforcement-rules.md` Claims to Verify `planned`).
- 아직 확인해야 할 사실: MapStruct generated mapper exemption 의 build path 가 실제 빌드 도구 설정에 따라 어떻게 달라지는지 (`feature-architecture-enforcement-rules.md` D9 `UNSUPPORTED_DECISION`).
- 과장하면 안 되는 부분: 본 글의 모든 검증은 **`locally-verified`** 다. ca-tmpl 은 template repository 이고 prod 운영 검증은 없다. 글에 "운영에서 검증된" 같은 표현 금지.
- 과장하면 안 되는 부분: "Gradle + ArchUnit 분업이 Spring Modulith 보다 우월하다" 가 아니라 "_작은 skeleton 에서는_ 가볍다" 까지만 주장 가능. Modulith verifier 를 도입한 사례 (kakaobank) 도 동등한 합리성을 가짐.
- 블로그로 쓰기 전에 필요한 canonical 정제: `wiki/projects/ca-tmpl/architecture-enforcement-rules.md` 를 최신 코드 상태 (모듈 매트릭스, ArchUnit rule 12종 이름) 로 맞춘 뒤 verified 항목만 blog 초안으로 이동.
## Decision / 처리 결정
- 액션: `promote-to-canonical`
- 이유: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 에 Gradle + ArchUnit boundary enforcement 글감으로 반영한다.
- 다음 단계: runtime lookup PoC / MapStruct exemption 같은 planned 항목은 blogify 전 과장 금지로 유지한다.
## Related / 관련
- 관련 branch: [[raw/branch-notes/feature-architecture-enforcement-rules]], [[raw/branch-notes/feature-skeleton-package-blueprint-contract]], [[raw/branch-notes/feature-application-port-usecase-contract]] (자매 결정).
- 관련 errors: [[raw/errors/archunit-empty-should-anchor-2026-05-27]], [[raw/errors/gradle-wrapper-readonly-cache-2026-05-28]], [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]].
- 관련 interview prep: [[raw/interviews/clean-architecture-boundary-enforcement]], [[raw/interviews/clean-architecture-module-blueprint]] (자매 질문), [[raw/interviews/transaction-port-vs-spring-transactional]] (`@Transactional` 다수파 vs 소수파 trade-off 단락의 자매).
- 관련 blog topics: [[raw/blog-topics/clean-architecture-module-blueprint-2026-05-28]] (자매 글감), [[raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28]] (다수파/소수파 trade-off 글감), [[raw/blog-topics/post-implementation-knowledge-capture-workflow-2026-05-28]] (캡처 워크플로우 자매).
- derived blog: 생성 전. 생성 시 `wiki/blog/clean-architecture-boundary-enforcement-YYYY-MM-DD.md` 후보.