--- kind: CASE slug: a19-f020-messaging-admin-api title: messaging-admin-api 의 시험 한 파일이 아홉을 담고 만료까지 단언한다 topic: messaging-and-outbox project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:a19-f020-messaging-admin-api evidenceCapturedOn: 2026-09-04 body: case-a19-f020-messaging-admin-api.body.md assets: - key: a19-f020-messaging-admin-api file: ../../../final/evidence/rendered/a19-f020-messaging-admin-api.svg evidence: - ../../../final/evidence/raw/a19-f020-messaging-admin-api.txt source: - 원본 분석 절은 analysis/19-messaging-platform.md#L997 이다. --- # messaging-admin-api 의 시험 한 파일이 아홉을 담고 만료까지 단언한다 `messaging-admin-api` 는 main 25 파일 1,613 줄인데 test 는 `DestructiveOperationGuardTest` 한 파일 147 줄이다. 그 한 파일이 담은 `@Test` 는 아홉이고, 만료된 승인에 대한 거부까지 그 안에 있다. ## 관계 - **messaging-reliability-api는 main 13파일 · 817 LOC에 테스트가 0개다** 두 기록 모두 같은 가족의 leaf 에서 main 대비 test 파일 수를 세는 데서 출발한다. 그 leaf 는 test 소스 세트 자체가 없어 계수가 그대로 결론이 되지만, 여기서는 시험 파일 하나를 열어 아홉이 무엇을 단언하는지 봐야 했다. - **브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다** 저 기록은 `BrokerAclManifest` 의 검사 메서드가 코드에 있는데 부르는 main 코드가 없는 경우다. 여기는 검증자를 만드는 코드가 시험 네 파일에만 있는데, 왜 그래야 하는지가 그 타입의 자바독에 적혀 있다. - **admin 스위치가 가드를 켜고 서비스는 켜지 않는다** 저 기록은 관리 스위치를 켜도 서비스 빈이 생기지 않는 것을 빈 목록에서 확인했다. 여기서는 같은 사슬의 가드가 `@Bean` 으로 존재하고 검증자만 test 에서 만들어진다는 것을 소스 세트로 확인했다. ## 문제 관리 평면의 계약 타입이 messaging-admin-api 에 스물다섯 개 있고 그 leaf 의 시험 파일은 하나다. 그 하나가 무엇을 덮고 나머지 검증이 어디 있는지 확인했다. ## 결론 원문이 적은 계수는 맞다. DestructiveOperationGuardTest 가 담은 @Test 는 아홉이다. 승인 요구와 만료 거부와 승인 통과가 각각 :63, :73, :102 에 있는데, 셋 다 단언이 걸리는 대상은 DestructiveOperationGuard 가 내는 예외 코드와 메시지다. 아홉 전부가 승인 객체를 한 헬퍼에서 얻는데, 그 헬퍼의 자바독은 아무나 생성할 수 있던 record 를 서명 없이는 얻을 수 없는 타입으로 바꾼 것이 목적이라고 적는다. 재구동의 자기 대상 금지(:113), 배치 상한(:119), TopologyManifest 의 차이 보고(:128, :140)도 같은 파일이다. 실제 시험은 대부분 messaging-admin-runtime 에 있다. 그 leaf 의 시험 6 파일 1,051 줄이 시험 51 개를 담고, 그중 ApprovalForgeryTest 열 개가 승인 위조 경로를 덮는다. HmacApprovalVerifier 를 생성하는 시험 파일은 두 leaf 에 넷이고 생성 지점은 다섯 줄이다. 그 타입을 만드는 main 코드는 0 건이다. 그것이 공백은 아니다. HmacApprovalVerifier:50~:53 의 자바독이 그 부재를 설계로 못박는다 — 키 보관자가 곧 발급자이므로 실행 런타임에 두어서는 안 된다는 것이다. 같은 사슬의 DestructiveOperationGuard 는 MessagingAdminAutoConfiguration:37 이 @Bean 으로 만들며 false 를 넘기는데, 그 인자가 같은 경계를 값으로 적은 것이다. PlanDigest, ApprovalGrant, TopologyManifest 는 그 이름을 건 시험 파일이 모두 0 개다. 세 타입을 참조하는 test 파일은 각각 6, 4, 4 인데 그 시험들은 이름이 가리키는 다른 대상을 단언한다. 판정은 P3 이고 원문과 같다. 원문이 미확인으로 남긴 예 둘 가운데 ApprovalGrant 의 만료는 DestructiveOperationGuardTest:73 과 ApprovalForgeryTest:158 이 단언하고, PlanDigest 의 정규화만 미확인으로 남는다. ## 검증 환경 확인 방식 : messaging-admin-api 와 messaging-admin-runtime 의 소스 세트 목록과 파일 수·줄 수 계수, 계약 leaf 시험 파일의 @Test 수와 메서드 이름 전수, 소비자 leaf 시험 파일별 @Test 수 계수와 ApprovalForgeryTest 메서드 전수, 계약 leaf 의 main 타입 전수, ApprovalVerifier 의 main 구현과 HmacApprovalVerifier 생성 지점을 leaf·소스 세트별로 분리, 그 타입의 자바독과 MessagingAdminAutoConfiguration 의 가드 빈 확인, 계약 타입 여섯의 main·test 참조 파일 수와 그 이름을 건 시험 파일 검색, PlanDigest 의 test 참조 전수 소스 수정 : x ## 재현 조건 1. messaging-admin-api 와 messaging-admin-runtime 의 소스 세트를 나열하고 각각의 파일 수와 줄 수를 센다. 2. 계약 leaf 의 시험 파일을 열고 @Test 수와 메서드 이름을 줄 순서대로 나열한다. 3. 각 이름이 무엇을 단언하는지 읽고 계약 타입과 짝짓는다. 4. 소비자 leaf 의 시험 파일마다 @Test 수를 세고, 승인 위조 시험의 메서드를 전부 읽는다. 5. 계약 leaf 의 main 타입을 전부 나열한다. 6. ApprovalVerifier 의 main 구현을 찾고, HmacApprovalVerifier 를 생성하는 자리를 leaf 와 소스 세트로 갈라 센다. 7. 그 타입의 자바독에서 main 에 두지 않는 이유가 적혀 있는지 읽는다. 8. 같은 사슬의 다른 타입이 자동 설정에서 빈으로 만들어지는지 확인하고 인자를 읽는다. 9. 계약 타입 몇 개의 main·test 참조 파일 수를 세고, 그 이름을 건 시험 파일을 *Test·*Tests·*IT 로 넓혀 찾는다. 10. 만료를 단언하는 자리를 검색하고, PlanDigest 가 test 에서 어떻게 쓰이는지 전수로 읽는다. ## 본문 `messaging-admin-api` 는 관리 평면의 계약 타입을 담는 leaf 다. main 25 파일 1,613 줄에 test 는 `DestructiveOperationGuardTest` 한 파일 147 줄이다. 그 비율만 보면 승인 사슬이 검증되지 않은 것처럼 읽힌다. ## DestructiveOperationGuardTest 한 파일이 담은 시험 아홉 :::evidence key="a19-f020-messaging-admin-api" alt="저장소 루트에서 돌린 정적 검색 출력 95줄. messaging-admin-api 와 messaging-admin-runtime 이 각각 main test 두 소스 세트만 갖고 25/1613·1/147, 12/1253·6/1051 이라는 계수가 먼저 나온다. 계약 leaf 의 유일한 시험 파일이 @Test 아홉을 담고 그 메서드 이름과 줄 번호가 줄 순서대로 나열되며, 이어서 그 시험들이 승인 객체를 얻는 통로인 헬퍼의 자바독과 verify 호출 줄, 각 단언이 기다리는 문자열, 그리고 가드가 그 문자열을 내는 자리가 나온다. 소비자 leaf 시험 여섯의 @Test 개수가 이어지고 그중 승인 위조 시험 열 개의 메서드 이름이 모두 나온다. 계약 leaf 의 main 타입 스물다섯이 이름으로 실리고, ApprovalVerifier 의 main 구현이 하나이며 HmacApprovalVerifier 생성 지점이 다섯 곳 파일 네 개로 전부 test 소스 세트라는 것이 leaf 이름과 함께 나온다. 이어서 그 검증자를 실행 런타임에 두지 않는 이유를 적은 자바독 네 줄과, 같은 사슬의 가드를 @Bean 으로 만드는 자동 설정 일곱 줄이 원문 그대로 실린다. 마지막으로 계약 타입 여섯의 main·test 참조 파일 수와 그 이름을 건 시험 파일이 모두 0 개라는 것, 만료를 고정하는 두 자리, 그리고 PlanDigest 가 test 에서 쓰이는 열두 줄이 나온다. 긴 문자열 리터럴은 가려져 있다." caption="두 leaf 의 소스 세트와 계수 · 계약 leaf 시험 아홉의 이름과 단언 대상 · 승인 객체를 얻는 헬퍼 · 소비자 시험 여섯과 위조 시험 열 · main 타입 스물다섯 · 검증자 생성 지점 다섯 곳 전부 test · 그것이 경계라고 적은 자바독과 main 빈으로 있는 가드 · 타입별 참조와 전용 시험 0 · 만료를 고정하는 두 자리 — 123줄 · exit 0" zoom="true" ::: 파일 이름은 `DestructiveOperationGuard` 만 가리키는데 `@Test` 는 아홉이다. 이름에 승인이 들어간 셋이 승인 판정을 단언한다. `anAdminRuntimeStillNeedsAnApproval`(`:63`)이 승인 없는 관리 런타임을 거부하는 것을, `anExpiredApprovalDoesNotAuthorise`(`:73`)가 만료된 승인이 권한을 주지 않는 것을, `anApprovedAdminOperationIsAuthorised`(`:102`)가 승인된 연산이 통과하는 것을 단언한다. 셋의 단언이 떨어지는 곳은 모두 `DestructiveOperationGuard` 다. `:69` 가 `"approval"` 을, `:87` 이 `"validity window"` 를 기다리는데 그 문자열은 `DestructiveOperationGuard:66` 의 `APPROVAL_REQUIRED` 와 `:70` 의 `APPROVAL_EXPIRED` 메시지에서 온다. `:109` 는 예외가 없는 것만 본다. 셋이 검증자를 지나는 정도는 서로 다르다. `:63` 은 `Optional.empty()` 를 넘기므로 승인 객체를 만들지 않는다. `:102` 는 정적 필드 `VALID` 를 쓰는데 그것은 클래스 로딩 때 한 번 만들어진다. `:73` 만 자기 시험 안에서 만료 구간을 지정해 승인 객체를 새로 만든다. 승인 객체를 만드는 통로는 `verified(...)` 헬퍼 하나이고 `:47` 에서 `ISSUER.verify(grant, ISSUER.sign(grant), digest, from)` 을 부른다. 그 헬퍼의 자바독(`:30`\~`:32`)은 가드가 예전에는 아무나 생성할 수 있는 `AdminApproval` 을 그대로 받았고, 지금은 모든 경우가 실제 서명을 지나야 승인 객체를 얻는다고 적는다. 타입을 바꾼 목적이 그것이라는 것이다. 나머지 여섯은 줄 순서대로 이렇다. `anApplicationRuntimeCannotRedrive`(`:51`), `aDryRunIsAlwaysPermitted`(`:91`), `aRedriveCannotTargetItsOwnSource`(`:113`), `aRedriveBatchIsBoundedSoOneOperationCannotFloodTheSource`(`:119`), `aTopologyManifestReportsEveryDifference`(`:128`), `aMatchingTopologyReportsNoDifferences`(`:140`) 다. 뒤의 둘은 `TopologyManifest` 가 차이를 전부 보고하는 경우와 일치할 때 하나도 보고하지 않는 경우를 짝으로 단언한다. ## messaging-admin-runtime 의 시험 6 파일이 담은 51 개 소비자 leaf 는 `messaging-admin-runtime` 하나이고 main 12 파일 1,253 줄에 시험 6 파일 1,051 줄이다. `@Test` 수는 `TopologyValidatorTest` 13, `ApprovedPlanExecutionTest` 11, `ApprovalForgeryTest` 10, `AdminOperationJournalTest` 8, `RedriveResumptionTest` 5, `TopologyValidationRuntimeTest` 4 로 합계 51 이다. `ApprovalForgeryTest` 열 개가 승인 사슬의 위조 경로를 덮는다. 검증자 밖에서 `VerifiedApproval` 을 만들 수 없다는 것(`:51`), 변조된 grant 가 검증되지 않는 것(`:69`), 다른 발급자의 서명이 검증되지 않는 것(`:84`), 한 계획의 승인이 다른 계획을 실행하지 못하는 것(`:96`), 만료된 grant 가 검증되지 않는 것(`:158`), 승인자와 운영자가 달라야 한다는 것(`:172`)이 각각 단언된다. ## HmacApprovalVerifier 를 main 에 두지 않는 것은 경계다 `ApprovalVerifier` 의 main 구현은 `HmacApprovalVerifier` 하나다. 그것을 생성하는 지점은 다섯 곳이고 파일은 넷이다. 하나는 `messaging-admin-api` 의 `DestructiveOperationGuardTest:21` 이고 나머지 셋은 `messaging-admin-runtime` 의 `ApprovalForgeryTest:42`·`:46`, `ApprovedPlanExecutionTest:37`, `RedriveResumptionTest:45` 다. main 에는 생성하는 코드가 없다. 그 부재가 검증 공백은 아니다. `HmacApprovalVerifier:50`\~`:53` 은 이 키를 쥔 쪽이 곧 발급자이며 그것이 연산을 실행하는 런타임에 있어서는 안 된다고 적는다. 같은 사슬의 다른 끝은 main 빈으로 있다. `MessagingAdminAutoConfiguration:35`\~`:41` 이 `@Bean @ConditionalOnMissingBean` 으로 `DestructiveOperationGuard` 를 만들면서 `false` 를 넘기고, 주석은 애플리케이션 런타임이 관리 자격을 갖지 않으므로 가드가 그런 연산을 거부한다고, 운영자 도구가 이 빈을 `true` 로 덮어쓴다고 적는다. 생성자에 넘기는 `false` 가 자바독이 말한 경계를 그대로 인코딩한 값이다. ## PlanDigest·ApprovalGrant·TopologyManifest 를 이름으로 건 시험 파일이 0 개다 계약 타입 여섯의 참조를 셌다. `PlanDigest` main 10 · test 6, `VerifiedApproval` main 7 · test 4, `TopologyManifest` main 5 · test 4, `ApprovalGrant` main 3 · test 4, `ApprovedRedrivePlan` main 2 · test 2, `ApprovedReplayPlan` main 2 · test 1 이다. 여섯 모두 그 이름을 건 시험 파일이 0 개다. `*Test.java` 뿐 아니라 이 저장소가 쓰는 `*IT.java` 와 `*Tests.java` 까지 넓혀 세도 0 이다. 만료는 두 자리가 고정한다. `DestructiveOperationGuardTest:73` 이 가드 쪽에서, `ApprovalForgeryTest:158` 의 `anExpiredGrantDoesNotVerify` 가 검증자 쪽에서 단언한다. `PlanDigest` 는 다르다. test 에서 나오는 열두 줄이 전부 `PlanDigest.ofCanonical(...)` 로 다이제스트를 만들거나 파라미터로 받는 자리다. 정규화 자체를 단언하는 줄은 없다. ## 원문과 갈리는 자리 원문은 계약 leaf 의 test 를 `1 (DestructiveOperationGuardTest)` 로만 적었다. 파일 이름은 그렇지만 담긴 아홉 중 셋이 승인 판정을, 둘이 `TopologyManifest` 를 단언한다. 원문은 계약 자체의 경계 조건이 별도로 고정돼 있는지 확인되지 않는다고 적으면서 `ApprovalGrant` 의 만료를 예로 들었다. 그 예는 `DestructiveOperationGuardTest:73` 과 `ApprovalForgeryTest:158` 이 고정한다. `PlanDigest` 의 정규화만 남는다. 원문 §8.2 는 main 참조가 0 인 다섯 중 넷에는 이유가 적혀 있지 않다고 하면서 `HmacApprovalVerifier` 를 그 넷에 넣었다. 이 타입에는 이유가 자기 자바독에 적혀 있다. ## 계약 타입 목록에 대해 원문은 계약 타입을 열 개 들고 `등` 으로 닫았다. 이 leaf 의 main 타입은 스물다섯이고, 그 열에 없는 `AdminOperationLease` 와 `AdminOperationState` 와 `DestinationTopology` 도 같은 스물다섯에 들어 있다. ## 확인하지 못한 것 시험을 한 번도 돌리지 않았다. 세고 읽는 데 그쳤다. 아홉을 셋과 여섯으로 나눈 기준은 메서드 이름과 단언 대상이다. 실행 경로를 계측해 가른 것이 아니다. 다른 이름을 단 시험이 `PlanDigest` 의 정규화를 고정하고 있을 가능성은 test 참조 열두 줄까지 읽고 좁혔다. 두 leaf 의 시험을 실행하지 않았다. 파일과 `@Test` 를 세고 메서드 이름과 단언 대상을 읽은 데까지다.