--- kind: CASE slug: a19-f022-messagingpublicsurfacecontracttest title: 공개 표면 계약 시험이 가족 밖 app-bootstrap 에 있다 topic: messaging-and-outbox project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:a19-f022-messagingpublicsurfacecontracttest evidenceCapturedOn: 2026-09-04 body: case-a19-f022-messagingpublicsurfacecontracttest.body.md assets: - key: a19-f022-messagingpublicsurfacecontracttest file: ../../../final/evidence/rendered/a19-f022-messagingpublicsurfacecontracttest.svg evidence: - ../../../final/evidence/raw/a19-f022-messagingpublicsurfacecontracttest.txt source: - 원본 분석 절은 final/document.md#a19#L1114 이다. --- # 공개 표면 계약 시험이 가족 밖 app-bootstrap 에 있다 지침이 이 규칙을 붙든다고 지목한 `MessagingPublicSurfaceContractTest` 가 가족 트리가 아니라 `src/app-bootstrap/src/test` 에 있다. 원문은 이것을 자동 검증이 없는 사례로 읽었는데, 경로 필터가 없는 `ci-quality-gates.yml` 이 모든 `pull_request` 에서 `./gradlew check` 를 돌리고 그 안에 이 시험이 들어 있다. ## 관계 - **아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다** 원문이 이 사례를 그 규칙으로 읽었다. 그런데 이 시험은 경로 필터 없는 워크플로에 실려 PR 마다 새로 도므로, 인용된 문장이 겨냥한 상태가 아니다. - **문서 계약 테스트의 단언 경계 밖에 발견된 드리프트 세 건이 전부 있었다** 둘 다 문서가 어떤 규칙을 시험이 지킨다고 적었는데, 그 시험이 실제로 붙드는 범위가 문서를 읽은 사람이 기대하는 범위보다 좁다. 저 기록은 단언 여덟 개 밖의 드리프트이고, 여기는 가족 태스크가 고르지 않는 소스 트리다. ## 문제 가족 지침은 :39 에서 이 규칙을 붙드는 것이 문서가 아니라 MessagingPublicSurfaceContractTest 라고 적는다. 그 시험이 어느 트리에 있고 어떤 경로로 실행되는지 확인했다. ## 결론 MessagingPublicSurfaceContractTest 가 놓인 트리는 src/app-bootstrap/src/test 이고 패키지는 dev.caskeleton.bootstrap.contract.messaging 이다. src/messaging 아래 24 개 트리에는 그 이름이 없다. 담긴 시험은 셋이고 @Tag 는 0 개다. 하나는 api 선언 누락을, 하나는 검사가 헛도는 경우를, 하나는 broker SDK 두 개의 컴파일 클래스패스 유출을 각각 잡는다. 가족 태스크는 이 클래스를 고르지 않는다. :messaging:messaging-kafka:test 에 이름을 넘기면 시험을 찾지 못하고 빌드가 실패한다. 가족 경로에서 도는 messaging-certification.yml 도 이 시험을 돌리지 않는다. 그 워크플로의 gradle 호출은 Kafka 인증 증거 검증 태스크 하나뿐이다. 그러나 원문이 읽은 것과 달리 자동 경로가 있다. ci-quality-gates.yml 은 paths 필터 없이 모든 pull_request 에서 돌고 :50 에서 ./gradlew check 를 돌린다. check 가 모든 레인을 덮지는 않는다는 경고가 그 아래 주석에 있어 이 프로젝트만 따로 확인했다. dry-run 그래프에 :app-bootstrap:test 가 있고, 그 태스크로 실행하면 test/ 아래에 tests=3 failures=0 이 남는다. 판정은 P3 이고 원문과 같다. 다만 근거가 다르다. 원문은 이 계약을 자동으로 검증하는 경로가 없다는 것을 들었는데, 검증은 매 PR 에서 돈다. 남는 것은 가족 태스크에 이 시험이 없어 로컬에서 위반을 보지 못한다는 것이다. ## 검증 환경 Gradle : 9.0.0 확인 방식 : 지침 문장 원문 확인, 시험 클래스의 소스 트리·패키지·태그 확인과 단언 메서드 전수, 가족 test 트리 계수, 가족 경로 워크플로의 트리거 전부와 실행 명령 확인, 경로 필터 없는 워크플로의 트리거·job 조건·실행 명령과 그 아래 주석 확인, :app-bootstrap:check 의 태스크 그래프 dry-run, 루트 test 규약의 제외 태그 확인, 그 시험을 이름으로 지정한 실제 실행과 결과 XML 계수, 가족 태스크에 같은 이름을 넘긴 실행 소스 수정 : x ## 재현 조건 1. 가족 지침에서 공개 표면 규칙을 붙든다고 적은 문장과 그 시험이 무엇을 대조하는지 문장 끝까지 읽는다. 2. 그 클래스를 찾아 소스 트리와 패키지를 확인하고, 가족 트리에 동명 파일이 있는지 센다. 3. 그 파일의 @Tag 와 @Disabled 를 세고 단언 메서드를 전부 읽는다. 4. 가족 경로에서 도는 워크플로의 트리거를 전부 읽고 실행 명령을 확인한다. 5. 경로 필터가 없는 워크플로를 찾고 job 조건과 실행 명령을 읽는다. 6. 그 명령 주변 주석에서 check 의 포괄 범위에 대한 경고가 있는지 본다. 7. check 를 dry-run 해 그 프로젝트의 test 가 그래프에 있는지 확인한다. 8. 루트 규약이 test 에 거는 제외 태그를 읽는다. 9. 그 시험을 이름으로 지정해 실제로 돌리고 결과 XML 의 디렉터리와 계수를 읽는다. 10. 같은 이름을 가족 태스크에 넘겨 돌린다. ## 본문 `src/messaging/CLAUDE.md:39` 는 의존성 노출 규칙을 문서가 아니라 `MessagingPublicSurfaceContractTest` 가 붙들고 있다고 적는다. 그 클래스는 `src/messaging` 아래에 없다. ## 지침이 지목한 시험과 그 트리 :::evidence key="a19-f022-messagingpublicsurfacecontracttest" alt="저장소 루트에서 돌린 정적 검색과 gradle 실행을 합친 출력 76줄. src/messaging/CLAUDE.md 의 36번부터 44번 줄이 원문 그대로 실려 규칙과 그 시험이 무엇을 대조하는지가 문장 끝까지 보인다. 이어서 그 클래스가 app-bootstrap 의 test 트리에 있고 src/messaging 아래 동명 파일이 0 개이며 가족 test 트리가 24 개라는 것, 그 파일의 @Test 가 3 이고 @Tag 와 @Disabled 가 0 이라는 것과 세 메서드 이름이 나온다. messaging-certification 워크플로의 트리거 세 가지가 경로와 cron 과 수동 실행까지 모두 나오고 그것이 돌리는 gradle 명령이 Kafka 인증 증거 검증이라는 것이 보인다. ci-quality-gates 는 on 블록 1번부터 9번 줄까지 실려 paths 필터가 0 건이라는 계수가 붙고, job 정의와 check 를 돌리는 step, 그리고 그 바로 아래 check 가 graphqlStableTest 에 의존하지 않는다고 적은 주석 다섯 줄이 함께 나온다. 마지막으로 app-bootstrap 의 check 를 dry-run 한 결과에 test 가 들어 있는 것, 루트 규약이 거는 유일한 필터, 그 시험을 이름으로 골라 실제로 돌린 결과가 결과 디렉터리 test 에 시험 3 개 실패 0 으로 나온 것, 같은 이름을 가족 태스크로 고르면 시험을 찾지 못하고 빌드가 실패하는 것이 나온다." caption="지침 원문 · 시험 클래스의 트리와 태그와 세 단언 · 가족 워크플로의 트리거 전부와 실행 명령 · 경로 필터 0 인 워크플로와 check 가 모든 시험을 덮지 않는다는 주석 · check 그래프에 있는 test · 이름으로 고른 실행 결과 3개 · 가족 태스크는 찾지 못함 — 76줄 · exit 0" zoom="true" ::: `CLAUDE.md:39`\~`:43` 은 이 시험이 leaf 의 production source 에서 public·protected 시그니처에 등장하는 vendor 라이브러리를 뽑아 그 leaf 의 `build.gradle` 이 `api` 로 선언했는지 대조하고, starter 의 `api` closure 에 broker client 둘이 들어오지 않는 것도 함께 본다고 적는다. 클래스는 `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/messaging/` 에 있다. 가족 test 트리는 24 개인데 그중 어디에도 같은 이름의 파일이 없다. `@Test` 는 셋이다. `vendorTypesInPublicSignaturesAreDeclaredApi`(`:73`)가 공개 시그니처의 vendor 타입이 `api` 로 선언됐는지 보고, `theScanIsNotVacuous`(`:94`)가 그 검사가 아무것도 못 찾은 채로 통과하는 경우를 막고, `neitherBrokerSdkReachesAnAdoptersCompileClasspath`(`:114`)가 두 broker SDK 가 채택자의 컴파일 클래스패스에 닿지 않는 것을 본다. ## 가족만 바꾸고 가족 태스크만 돌릴 때 `:messaging:*:test` 에 이 클래스 이름을 넘겨 봤다. `No tests found for given includes` 로 빌드가 실패한다. 가족 경로에서 도는 워크플로는 `messaging-certification.yml` 이다. `:16`\~`:19` 가 `src/messaging/**` 와 자기 워크플로 파일에서 돌게 하고 `:20`\~`:22` 가 주 1회 cron 과 수동 실행을 더한다. `:52` 가 돌리는 것은 `:messaging:messaging-kafka:verifyMessagingCertificationEvidence` 다. Kafka 인증 증거이지 이 계약이 아니다. ## ci-quality-gates.yml 은 경로 필터 없이 매 PR 에서 check 를 돌린다 `:3`\~`:9` 의 `on` 블록은 `pull_request` 와 `main` 푸시와 수동 실행이고 `paths` 가 0 건이다. `:20` 의 `quality-gates` job 에 조건이 없고, `:49`\~`:50` 이 `working-directory: src` 에서 `./gradlew check verifyPublicPathSnapshot verifyDependencyLocks` 를 돌린다. `check` 가 모든 시험 태스크를 덮는다고 가정할 수는 없다. 바로 아래 `:51`\~`:55` 주석이 `check` 는 `graphqlStableTest` 에 의존하지 않으며 그래서 그 레인의 가드가 CI 에서 아무것도 지키지 못했다고 적는다. 그래서 이 프로젝트에 대해 직접 확인했다. `:app-bootstrap:check` 의 태스크 그래프에 `:app-bootstrap:test` 가 있다. 루트 규약(`src/build.gradle:549`\~`:553`)이 `test` 에 거는 필터는 `excludeTags 'quarantine'` 하나이고 이 클래스에는 `@Tag` 가 0 개다. 그리고 이름으로 골라 실제로 돌렸다. `BUILD SUCCESSFUL` 이고 결과가 `test/` 디렉터리에 `tests=3 failures=0 skipped=0` 으로 남는다. 기본 `test` 가 이 시험을 고른다. ## 원문과 갈리는 자리 원문은 결과를 둘로 적었다. 가족 태스크만 돌리면 검증되지 않는다는 것과 가족 경로 워크플로가 Kafka 인증 레인이라는 것이다. 둘 다 맞다. 원문은 그 둘에서 이 사례가 모듈 18 §4.1c 의 문장 — 아무도 지역에서 돌리지 않는 레인의 붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다 — 과 같은 형태라고 읽었다. 그 문장은 여기에 붙지 않는다. `ci-quality-gates.yml` 이 경로 필터 없이 매 PR 에서 `check` 를 돌리고, 그 안에 이 시험이 들어 있다. 이 게이트가 보고하는 것은 마지막으로 돌린 사람이 본 것이 아니라 그 PR 에서 새로 돈 결과다. `api`·`implementation` 분리가 이 가족의 정책이고 위반이 이 가족의 `build.gradle` 에서 난다는 서술은 맞다. 다른 것은 검증의 유무가 아니라 그 검증이 실행되는 시점이다. 가족 태스크에는 이 시험이 없고 `ci-quality-gates.yml` 의 `check` 에만 있다. ## 확인하지 못한 것 가족 leaf 에 `api` 누락을 심어 `:messaging:*:test` 가 초록으로 끝나는 것을 재현하지 않았다. 그 태스크가 이 클래스 이름을 찾지 못한다는 것까지 실행으로 확인했다. GitHub 러너에서 `./gradlew check` 를 돌리지 않았다. 워크플로가 그 명령을 돌린다는 것, `check` 의 그래프에 `:app-bootstrap:test` 가 있다는 것, 그 태스크가 이 시험을 고른다는 것을 각각 확인해 이었다. 워크플로 스물여덟 개의 트리거를 전수로 조사하지 않았다. 이 계약과 관련된 둘을 읽었다. 워크플로 스물여덟 개의 트리거를 전수로 조사하지 않았다. 이 계약과 관련된 둘을 읽었다.