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

11 KiB

title, source_type, status, related_branches, related_projects, tags, created, status_label
title source_type status related_branches related_projects tags created status_label
error / archunit-test-scope-sample-ticket-inclusion-2026-05-28 error-note raw
feature-application-port-usecase-contract
ca-skeleton
error
ca-skeleton
archunit
test-scope
gradle
sample-ticket
2026-05-28 resolved

error: archunit-test-scope-sample-ticket-inclusion-2026-05-28

Layer: raw/errors/ — 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw 에 영구 보관.

Parent / 부모

증상 / Symptom

  • 에러 메시지 (원문 그대로):
    BUILD SUCCESSFUL in 5s
    16 actionable tasks: 3 executed, 13 up-to-date
    
    기대값application_does_not_use_spring_transactional_annotation rule 의 실패 (당시 sample-ticket/.../UserServicePostServiceorg.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.
  • 재현 가능 여부: alwaysapp-bootstrap 의 production dependency 매트릭스에 sample-ticket 이 없는 상태에서 ArchUnit rule 이 ..application.. 패키지를 검사하면 재현.

재현 절차 / Reproduction

  1. ca-tmpl src/build.gradleallowedProjectDependencies 매트릭스에서 app-bootstrap 의 allowed 목록에 sample-ticket없음 을 확인.
  2. src/sample-ticket/.../application/UserService.javaimport org.springframework.transaction.annotation.Transactional;있음 을 확인.
  3. src/app-bootstrap/.../CleanArchitectureTest.java 에 다음 rule 을 추가:
    @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.gradleallowedProjectDependencies 매트릭스 확인 → 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-bootstraptest JVM classpath없음. ArchUnit 가 패키지 필터 dev.caskeleton 으로 import 해도 sample-ticket 의 클래스를 못 봄. 그래서 rule 이 0 개의 application 클래스 를 평가했고, allowEmptyShould(true)true 로 해석.
  • 2026-05-28 — 해결 후보 검토:
    • (a) app-bootstrapimplementation project(':sample-ticket') 추가 → production dependency 매트릭스 위반, verifyCleanArchitectureDependencies 실패. 채택 안 함.
    • (b) app-bootstraptestImplementation project(':sample-ticket') 추가 → production 매트릭스 영향 없음 (['api', 'implementation', 'compileOnly', 'runtimeOnly'] 만 검사). ArchUnit production_code_does_not_depend_on_sample_ticketImportOption.DoNotIncludeTeststest 클래스 제외 하므로 production drift 로 잘못 보고 안 됨. 채택.
  • 2026-05-28 — app-bootstrap/build.gradletestImplementation project(':sample-ticket') 추가 후 재실행 → 이제는 sample-ticket 클래스가 ArchUnit scope 에 잡혀서 application_does_not_use_spring_transactional_annotation rule 이 실패 (예상대로).
  • 2026-05-28 — sample-ticketUserService / 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 / 근거

해결 / Resolution

  • 적용한 조치: src/app-bootstrap/build.gradledependencies 블록에 testImplementation project(':sample-ticket') 추가. 주석으로 비대칭 의존의 의도를 명시:
    // 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.gradletestImplementation 에 명시 추가 + 주석으로 비대칭 의존 이유 기록.
    • 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 에 일반적으로 적용 가능.