Files
llm-wiki/raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28.md

119 lines
11 KiB
Markdown

---
title: error / archunit-test-scope-sample-ticket-inclusion-2026-05-28
source_type: error-note
status: raw
related_branches: [feature-application-port-usecase-contract]
related_projects: [ca-skeleton]
tags: [error, ca-skeleton, archunit, test-scope, gradle, sample-ticket]
created: 2026-05-28
status_label: resolved
---
# error: archunit-test-scope-sample-ticket-inclusion-2026-05-28
> Layer: `raw/errors/` — 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw 에 영구 보관.
## Parent / 부모
- [[raw/branch-notes/feature-application-port-usecase-contract]] — `application_does_not_use_spring_transactional_annotation` ArchUnit rule 추가 중 vacuously 통과한 함정 + `testImplementation project(':sample-ticket')` 으로 해결한 경험.
- [[raw/project-notes/ca-skeleton-operational-contract]] — ca-tmpl skeleton 의 boundary 자동 검증 정책 맥락.
## 증상 / Symptom
- 에러 메시지 (원문 그대로):
```text
BUILD SUCCESSFUL in 5s
16 actionable tasks: 3 executed, 13 up-to-date
```
_기대값_ 은 `application_does_not_use_spring_transactional_annotation` rule 의 _실패_ (당시 `sample-ticket/.../UserService` 와 `PostService` 가 `org.springframework.transaction.annotation.Transactional` 을 import 중). 그러나 BUILD SUCCESSFUL — rule 이 _vacuously_ 통과. ArchUnit 의 "failed to check any classes" 에러조차 _뜨지 않음_ (rule 이 정상 평가됐다고 인식).
- 발생 컨텍스트: `cd src && ./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest'`. 새 ArchUnit rule 3종 (`application_does_not_use_spring_transactional_annotation`, `inbound_port_implementations_end_with_use_case`, `inbound_port_implementations_declare_capability`) 추가 직후 첫 실행.
- 발생 시점: 2026-05-28.
- 발생 환경: ca-tmpl repository, local Linux.
- 재현 가능 여부: `always` — `app-bootstrap` 의 production dependency 매트릭스에 `sample-ticket` 이 없는 상태에서 ArchUnit rule 이 `..application..` 패키지를 검사하면 재현.
## 재현 절차 / Reproduction
1. ca-tmpl `src/build.gradle` 의 `allowedProjectDependencies` 매트릭스에서 `app-bootstrap` 의 allowed 목록에 `sample-ticket` 이 _없음_ 을 확인.
2. `src/sample-ticket/.../application/UserService.java` 에 `import org.springframework.transaction.annotation.Transactional;` 가 _있음_ 을 확인.
3. `src/app-bootstrap/.../CleanArchitectureTest.java` 에 다음 rule 을 추가:
```java
@ArchTest
static final ArchRule application_does_not_use_spring_transactional_annotation =
noClasses()
.that().resideInAPackage("..application..")
.should().dependOnClassesThat().haveFullyQualifiedName(
"org.springframework.transaction.annotation.Transactional"
)
.allowEmptyShould(true);
```
4. `cd src && ./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest'` 실행.
5. 기대 결과: `application_does_not_use_spring_transactional_annotation` rule 이 _실패_ (UserService / PostService 가 위반).
6. 실제 결과: BUILD SUCCESSFUL. rule 이 _vacuously_ 통과.
## 조사 단계 / Investigation log
- 2026-05-28 — ArchUnit test 실행 → 모든 rule 통과 → 의외. `sample-ticket` 의 `@Transactional` 이 분명히 남아 있는데?
- 2026-05-28 — `CleanArchitectureTest.java` 의 `@AnalyzeClasses(packages = "dev.caskeleton", importOptions = ImportOption.DoNotIncludeTests.class)` 확인 → 패키지 필터는 `dev.caskeleton` 이지만, _실제 import 대상은_ `app-bootstrap` 의 test classpath 에 _존재_ 하는 클래스 중 패키지 필터 매치 부분이다.
- 2026-05-28 — `src/build.gradle` 의 `allowedProjectDependencies` 매트릭스 확인 → `app-bootstrap` 의 allowed = `['domain-core', 'application-core', 'adapter-web', 'adapter-persistence', 'adapter-outbound', 'shared-contract']` — `sample-ticket` 은 _의도적으로 부재_ (production 역수입 금지의 자매 결정).
- 2026-05-28 — 결론: `sample-ticket` 의 main 클래스는 `app-bootstrap` 의 _test JVM classpath_ 에 _없음_. ArchUnit 가 패키지 필터 `dev.caskeleton` 으로 import 해도 `sample-ticket` 의 클래스를 못 봄. 그래서 rule 이 _0 개의 application 클래스_ 를 평가했고, `allowEmptyShould(true)` 가 _true_ 로 해석.
- 2026-05-28 — 해결 후보 검토:
- (a) `app-bootstrap` 에 `implementation project(':sample-ticket')` 추가 → production dependency 매트릭스 위반, `verifyCleanArchitectureDependencies` 실패. 채택 안 함.
- (b) `app-bootstrap` 에 `testImplementation project(':sample-ticket')` 추가 → production 매트릭스 영향 없음 (`['api', 'implementation', 'compileOnly', 'runtimeOnly']` 만 검사). ArchUnit `production_code_does_not_depend_on_sample_ticket` 도 `ImportOption.DoNotIncludeTests` 로 _test 클래스 제외_ 하므로 production drift 로 잘못 보고 안 됨. **채택**.
- 2026-05-28 — `app-bootstrap/build.gradle` 에 `testImplementation project(':sample-ticket')` 추가 후 재실행 → 이제는 `sample-ticket` 클래스가 ArchUnit scope 에 잡혀서 `application_does_not_use_spring_transactional_annotation` rule 이 _실패_ (예상대로).
- 2026-05-28 — `sample-ticket` 의 `UserService` / `PostService` 의 `@Transactional` 을 모두 `TransactionPort` 호출로 치환 → 재실행 → BUILD SUCCESSFUL. red/green 검증 완료.
## 근본 원인 / Root cause
- 직접 원인: `app-bootstrap` 의 production dependency 매트릭스에 `sample-ticket` 이 없어서 `sample-ticket` 의 main 클래스가 `app-bootstrap` 의 test JVM classpath 에 없었음. ArchUnit 의 `@AnalyzeClasses(packages = "dev.caskeleton")` 는 패키지 _필터_ 일 뿐 _classpath scan source_ 가 아님.
- 근본 원인: ArchUnit 의 import scope 가 _현재 모듈의 컴파일 + 런타임 classpath_ 에 의존한다는 점을 _패키지 필터_ 만 보면 놓치기 쉬움. 패키지 필터가 "이 패키지를 검사한다" 의 _전제_ 가 아니라 _필터_ 임을 인식 못 함.
- 트리거 조건: `sample-ticket` 이 production 역수입 금지 정책에 따라 `app-bootstrap` 의 production dep 가 _아님_ (의도된 정책) + ArchUnit rule 이 `..application..` 패키지를 검사 (sample-ticket 도 이 패키지에 포함됨) 의 _교차_ 상황.
## Sources / 근거
- [[raw/branch-notes/feature-application-port-usecase-contract]] — 본 에러를 발견한 작업의 branch-note + Decisions 2026-05-28.
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — `production_code_does_not_depend_on_sample_ticket` rule 의 `ImportOption.DoNotIncludeTests` 사용 (D7).
- [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] — `sample-ticket` 의 production 역수입 금지 결정 (D7).
- [[raw/errors/archunit-empty-should-anchor-2026-05-27]] — ArchUnit 의 _빈 평가_ 와 `allowEmptyShould(true)` 의 또 다른 함정 사례.
- ca-tmpl 코드: `src/build.gradle` `verifyCleanArchitectureDependencies` task — `['api', 'implementation', 'compileOnly', 'runtimeOnly']` 만 검사. `testImplementation` 은 production 매트릭스에서 _제외_.
- ca-tmpl 코드: `src/app-bootstrap/.../CleanArchitectureTest.java` `@AnalyzeClasses(importOptions = ImportOption.DoNotIncludeTests.class)`.
## 해결 / Resolution
- 적용한 조치: `src/app-bootstrap/build.gradle` 의 `dependencies` 블록에 `testImplementation project(':sample-ticket')` 추가. 주석으로 비대칭 의존의 의도를 명시:
```gradle
// sample-ticket is on the test classpath only so the ArchUnit suite can analyse the
// template's reference implementation. Production scope MUST NOT depend on
// sample-ticket; that rule is enforced by `production_code_does_not_depend_on_sample_ticket`.
testImplementation project(':sample-ticket')
```
- 검증 방법:
- `cd src && ./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest'` — `application_does_not_use_spring_transactional_annotation` 가 _실패_ 시키는지 확인 (`sample-ticket` migration 전).
- sample-ticket migration 후 동일 명령 실행 → BUILD SUCCESSFUL. red/green 양쪽 확인.
- `cd src && ./gradlew verifyCleanArchitectureDependencies` — production 매트릭스 영향 없음 확인.
- `cd src && ./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest'` 의 `production_code_does_not_depend_on_sample_ticket` rule — 여전히 통과 (test 클래스 제외 옵션 때문).
- 잔여 위험 / 후속 작업:
- 다른 sample / fixture 모듈이 추가될 경우 동일 패턴 (`testImplementation project(':<fixture>')`) 을 명시적으로 적용해야 함. 누락되면 vacuously pass 재발 가능.
- ArchUnit rule 을 추가할 때 _수반_ 해야 할 체크리스트 (해당 rule 이 잡으려는 _violating example_ 이 test classpath 에 있는지) 가 명시화되지 않음 — 후속 review checklist 후보.
## 회고 / Lessons
- 빨리 감지하는 신호:
- ArchUnit rule 을 추가했는데 _실패할 거라고 100% 확신_ 한 케이스가 통과하면 _rule 이 잘못된 게 아니라 import scope 가 비어 있을_ 가능성을 첫 의심.
- `BUILD SUCCESSFUL` + 새 rule 의 `failed to check any classes` 경고조차 _없음_ → 패키지 필터에 매치되는 클래스가 _classpath 에 없는_ 상태.
- 새 rule 을 PR 에 넣기 전 _임시 violating code_ 를 추가해 _red 가 되는지_ 확인 (`feature-architecture-enforcement-rules.md` 의 red/green 패턴과 동일).
- 예방 체크리스트 항목 후보:
- 새 ArchUnit rule 추가 시 _이 rule 이 잡으려는 위반 예시가 ArchUnit 의 import scope (= 현재 모듈의 test JVM classpath) 에 실제로 존재하는가_ 를 먼저 확인.
- 새 sample / fixture 모듈 추가 시 `app-bootstrap/build.gradle` 의 `testImplementation` 에 명시 추가 + 주석으로 비대칭 의존 이유 기록.
- ArchUnit `@AnalyzeClasses(packages = ...)` 가 _필터_ 일 뿐 _scan source_ 가 아니라는 사실을 PR 리뷰 checklist 에 추가.
- wiki 로 끌어올릴 가치가 있는 일반화된 교훈:
- ArchUnit 의 _scope = classpath ∩ package filter_. 둘 중 하나가 비어 있으면 vacuously pass.
- production dependency 차단 (`production_code_does_not_depend_on_sample_ticket`) 과 test-scope inclusion (`testImplementation project(':sample-ticket')`) 의 _비대칭 의존_ 패턴은 sample / fixture module 이 있는 multi-module repo 에 일반적으로 적용 가능.
## Related / 관련
- 트리거된 daily note: [[raw/daily-notes/2026-05-28]].
- 관련 branch note: [[raw/branch-notes/feature-application-port-usecase-contract]], [[raw/branch-notes/feature-architecture-enforcement-rules]] (자매 — boundary 자동 검증).
- 관련 errors: [[raw/errors/archunit-empty-should-anchor-2026-05-27]] (ArchUnit empty pass 의 또 다른 변종).
- 관련 wiki 개념: [[wiki/concepts/clean-architecture-package-layout]] (아직 갱신 전), 후보 [[wiki/concepts/archunit-scope-classpath-vs-package-filter]] (정제 시 신규).
- 관련 blog topics: [[raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28]] (본 에러를 발견한 작업의 글감), [[raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28]] (boundary 자동 검증 글감).