112 lines
11 KiB
Markdown
112 lines
11 KiB
Markdown
---
|
||
title: blog-topic / archunit-violations-as-data-pattern-2026-05-28
|
||
source_type: blog-topic
|
||
status: raw
|
||
related_branches: [feature-architecture-enforcement-rules, feature-application-port-usecase-contract]
|
||
related_projects: [ca-tmpl]
|
||
tags: [blog-topic, ca-tmpl, archunit, testing, fitness-function, spring-modulith, negative-test]
|
||
created: 2026-05-28
|
||
status_label: ready-for-canonical
|
||
target_audience: backend-engineer
|
||
inspiration_url:
|
||
archive_url:
|
||
---
|
||
|
||
# blog-topic: archunit-violations-as-data-pattern-2026-05-28
|
||
|
||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||
|
||
## Parent / 부모
|
||
|
||
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — `violations-as-data` pattern 을 Claims to Verify 의 `planned` 에서 `actually-implemented` 로 승급시킨 round 2 작업.
|
||
- [[raw/branch-notes/feature-application-port-usecase-contract]] — D14 (KEYED idempotency freeze) 의 ArchUnit custom condition 도 같은 fixture 로 보증.
|
||
|
||
## 트리거 / Trigger
|
||
|
||
- 트리거 유형: `branch-work`
|
||
- 트리거 날짜: 2026-05-28
|
||
- 트리거 연결 노트: [[raw/branch-notes/feature-architecture-enforcement-rules]] — Claims to Verify 의 마지막 행 ("ArchUnit rule 이 위반을 실제로 catch 한다는 commit 된 증명") 을 Spring Modulith `example/ninvalid` 패턴으로 구현한 작업.
|
||
|
||
## 글감 / Topic seed
|
||
|
||
- 한 문장 요지: ArchUnit rule 은 _없는 위반_ 에 대해 vacuously pass 한다 — production 코드에 위반이 우연히 없을 때도, 분석 scope 자체가 비어있을 때도 동일하게 SUCCESS. **위반 fixture + negative test** 로 _rule 이 실제로 catch 하는지_ 를 commit 으로 박아두지 않으면 silent regression 이 누적된다.
|
||
- 떠오른 계기: round 1 작업에서 발견한 [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] (ArchUnit scope 가 production classpath 만 보고 vacuous pass 한 사례) + Spring Modulith 의 `example/ninvalid` 패턴 발견.
|
||
- 예상 제목 후보:
|
||
- ArchUnit rule 을 _믿을 수 있게_ 만드는 violations-as-data 패턴
|
||
- Spring Modulith 의 `example/ninvalid` 를 ca-skeleton 에 차용한 6개 negative test
|
||
- "rule 이 작동하는지" 를 commit 으로 박아두기 — fitness function 의 self-verification
|
||
|
||
## 핵심 주장 후보 / Claim candidates
|
||
|
||
- 사실 후보:
|
||
- ArchUnit 의 vacuous pass 함정은 두 갈래로 발생 — (a) production code 에 위반이 우연히 없음, (b) `@AnalyzeClasses` 의 import scope 가 비어 있음 (classpath 누락) — 근거: [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] §직접 원인.
|
||
- Spring Modulith 공식 incubator 가 _자기 rule 들을 검증_ 하기 위해 `example/ninvalid` fixture package 와 `modules.detectViolations().getMessages()` assertion 을 사용 — 근거: `raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data.md#SPRING-MOD-AU-C2` (`SPRING-MOD-AU-C2`).
|
||
- ca-tmpl 의 적용: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/violations/` 에 6 fixture (domain 1 + application 5) + `ArchitectureViolationFixtureTest` 에 6 negative test — 근거: `feature-architecture-enforcement-rules.md` 구현 결과 round 2 + Claims to Verify 마지막 행 → `actually-implemented`.
|
||
- fixture 는 `src/test/...` 위치이므로 main `@AnalyzeClasses(importOptions = DoNotIncludeTests.class)` 에 _자동으로_ 제외됨 — main suite 가 fixture 때문에 실패하지 않음 — 근거: `feature-architecture-enforcement-rules.md` 진행 중 메모 round 2.
|
||
- negative test 는 `new ClassFileImporter().importPackages("...violations")` 로 fixture _만_ 로드한 뒤 rule 의 `EvaluationResult.hasViolation() == true` 를 단순 assert — 근거: `src/app-bootstrap/src/test/.../ArchitectureViolationFixtureTest.java`.
|
||
- 경험 후보:
|
||
- KEYED idempotency rule (D14) 은 ArchUnit DSL 로 표현 불가능 → custom `ArchCondition<JavaClass>` 작성. `JavaAnnotation.get("idempotency")` 가 `JavaEnumConstant` 를 반환하므로 bytecode 만으로 enum value 검사 가능 (reflection 없음) — 근거: `CleanArchitectureTest#notDeclareKeyedIdempotency` 메서드 + branch-note D14.
|
||
- `TransactionalAnnotatedFixture` 가 `@Transactional` 을 import 하려면 `app-bootstrap/build.gradle` 의 `testCompileOnly 'org.springframework:spring-tx'` 가 필요. production scope 에 영향 없음 (test 만) — 근거: `feature-architecture-enforcement-rules.md` round 2 메모.
|
||
- fixture 클래스를 `package-private` 으로 유지해 _외부 사용 불가_ 명시 + ArchUnit fixture import 만 동작 — 의도 노이즈 차단.
|
||
- 의견 / 해석 후보:
|
||
- ArchUnit rule 은 _코드_ 다. 코드는 테스트 없이 믿으면 안 된다 — fitness function 도 동일.
|
||
- vacuous pass 는 _"rule 이 안 잡혔다"_ 가 아니라 _"rule 이 무엇을 잡는지 아무도 검증 안 했다"_ 의 신호. 본 패턴은 후자를 commit 으로 박는 게 목적.
|
||
- **간단한 rule (`noClasses().that(pkg).should().dependOn(pkg2)`) 은 vacuous pass 위험이 _제일 큼_** — 술어가 단순할수록 production 매칭이 우연히 0개가 되기 쉽다. _복잡한 custom condition (D14) 은 명시적으로 짠 거니까 더 안전_ 이라는 직관과 반대.
|
||
- Spring Modulith 가 _자기 자신_ 을 검증하는 데 쓰는 패턴이라는 점이 글의 강한 thesis — "rule 의 production-readiness 의 골든 스탠다드".
|
||
|
||
## Outline seed
|
||
|
||
1. 동기 — ArchUnit 의 vacuous pass 함정 두 갈래 → **rule 이 잡는다고 _믿는_ 것과 _증명_ 하는 것의 차이.**
|
||
2. Spring Modulith 의 self-verification 패턴 (`example/ninvalid` + `detectViolations().getMessages()`) → **OSS 공식 incubator 가 자기 rule 을 같은 방식으로 검증한다는 신뢰 신호.**
|
||
3. ca-tmpl 의 차용 — `violations/` package + `ArchitectureViolationFixtureTest` → **6 fixture × 6 negative test 의 1:1 매칭.**
|
||
4. fixture 의 위치 결정 — `src/test/...` 안에 두면 main `DoNotIncludeTests` 가 자동 제외 → **main suite 와 negative test 가 _서로를 깨뜨리지 않는_ 격리.**
|
||
5. custom ArchCondition (D14) — `JavaAnnotation.get(...)` 로 enum value 검사 → **reflection 없이 bytecode 만으로 annotation parameter catch 가능.**
|
||
6. fixture 의 deps — `testCompileOnly 'org.springframework:spring-tx'` 의 비대칭 의존 → **`@Transactional` 을 _import_ 만 하고 production scope 에는 안 들어감.**
|
||
7. 한계 — string-key bean lookup / `Class.forName(String)` 의 bypass 는 여전히 catch 불가 (D12) → **fitness function 의 정직한 한계.**
|
||
8. 정리 — rule 은 코드다. 코드는 negative test 없이 믿지 말자.
|
||
|
||
## Canonical 전환 후보 / Canonical extraction candidates
|
||
|
||
- `wiki/projects/ca-tmpl/archunit-violations-as-data.md` 후보:
|
||
- 실제 6 fixture + 6 negative test 의 코드 발췌.
|
||
- `testCompileOnly 'org.springframework:spring-tx'` 비대칭 의존 패턴.
|
||
- `JavaAnnotation.get("idempotency")` custom condition 코드.
|
||
- `wiki/concepts/archunit-violations-as-data.md` 후보:
|
||
- "vacuous pass 함정 두 갈래" 의 project-agnostic 정리.
|
||
- Spring Modulith `example/ninvalid` 패턴의 일반화.
|
||
- test-scope fixture + main DoNotIncludeTests 의 격리 패턴.
|
||
- 필요한 추가 검증:
|
||
- 본 패턴이 _큰 codebase_ (rule 수십 개) 에 적용했을 때 negative test 가 production rule 의 작은 변경에 같이 깨지는지 (regression sensitivity 측정).
|
||
- `feature-archunit-negative-fixture-baseline` 같은 후속 branch 를 분리할 가치가 있는지.
|
||
|
||
## Sources / 근거 후보
|
||
|
||
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — Claims to Verify 마지막 행 `actually-implemented` 승급 + 진행 중 메모 round 2 + Closure 갱신.
|
||
- [[raw/branch-notes/feature-application-port-usecase-contract]] — D14 의 custom ArchCondition 으로 KEYED enum value catch.
|
||
- [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] — vacuous pass 의 두 번째 갈래 (classpath scope) 의 실 사례.
|
||
- [[raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data]] — `example/ninvalid` 패턴 (`SPRING-MOD-AU-C2`) + `annotatedWith(Generated.class)` 예시 (`SPRING-MOD-AU-C1`).
|
||
- [[raw/official-docs/archunit-user-guide]] — `JavaAnnotation` / `JavaEnumConstant` / `EvaluationResult.hasViolation()` 의 공식 API 근거 (`ARCHUNIT-UG-C5`).
|
||
- [[raw/interviews/archunit-static-analysis-limits]] — 같은 작업에서 파생된 면접 질문.
|
||
|
||
## 미해결 / Unknown
|
||
|
||
- 아직 확인해야 할 사실: negative test 가 _rule wording 변경_ 에 얼마나 민감하게 깨지는지 — false-positive regression 비용 vs 진짜 regression catch 비용의 균형.
|
||
- 아직 확인해야 할 사실: fixture 클래스의 _수_ 가 늘어날 때 (예: 50 rule × 50 fixture) 관리 비용. Spring Modulith 의 실 fixture 디렉터리 크기와 비교 필요.
|
||
- 과장하면 안 되는 부분: 본 패턴은 _ArchUnit static analysis 의 한계 (D12 string bypass)_ 를 보완하지 _않는다_. negative test 도 정적이라 reflection bypass 는 잡지 못함.
|
||
- 과장하면 안 되는 부분: ca-tmpl 의 6 fixture / 6 test 는 _proof-of-concept_ 규모. 실 사업 도메인의 30+ rule 적용 시 관리 비용 측정 미수행.
|
||
- 블로그로 쓰기 전에 필요한 canonical 정제: `wiki/projects/ca-tmpl/archunit-violations-as-data.md` + `wiki/concepts/archunit-violations-as-data.md` 정제. 1~2개 추가 branch 적용 사례 누적.
|
||
|
||
## Decision / 처리 결정
|
||
|
||
- 액션: `promote-to-canonical`
|
||
- 이유: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 와 `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 에 violations-as-data / negative fixture 글감으로 반영한다.
|
||
- 다음 단계: blogify 전 적용 branch 수와 fixture 관리 비용을 확인한다.
|
||
|
||
## Related / 관련
|
||
|
||
- 관련 branch: [[raw/branch-notes/feature-architecture-enforcement-rules]] (본 패턴의 직접 적용), [[raw/branch-notes/feature-application-port-usecase-contract]] (D14 custom condition 의 negative test).
|
||
- 관련 errors: [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] (vacuous pass 의 다른 갈래).
|
||
- 관련 interview prep: [[raw/interviews/archunit-static-analysis-limits]] (static analysis 한계 + violations-as-data 보완), [[raw/interviews/clean-architecture-boundary-enforcement]] (선행 — boundary 자동 검증의 자매 토픽).
|
||
- 관련 blog topics: [[raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28]] (boundary 자동 검증의 자매 글감).
|
||
- derived blog: 생성 전. 생성 시 `wiki/blog/archunit-violations-as-data-pattern-YYYY-MM-DD.md` 후보.
|