{ "schema_version": "1.0", "document": "/home/donghyeon/workspace/chat-gpt-container/document-haness/docs/clean-architecture-backend-template/final/document.md", "document_sha256": "8071fe71b3359d9cf60b95909c26c7b50653ce2f22bbc5fcf6988719bb91236d", "line_count": 47035, "line_number_space": "canonical-source-with-managed-blocks-collapsed", "anchor": { "kind": "line", "value": 40172, "line": 40172 }, "current_section": { "heading": { "line": 40172, "level": 4, "text": "11. 빌드/ArchUnit/CI 강제 지점" }, "start_line": 40172, "end_line": 40233, "text": "#### 11. 빌드/ArchUnit/CI 강제 지점\n\n`jmh` 소스셋이 루트에서 정확히 3개 리프에만 부여된다.\n\n```groovy\n// src/build.gradle:500-508\n// The three messaging leaves that carry JMH benchmarks get a `jmh` source set. It is a source\n// set rather than a plugin because the benchmarks are compiled and reviewed on every build but\n// only *run* on demand: a benchmark that stops compiling is a defect, while a benchmark that\n// runs in CI is a flaky test measuring the build agent.\nif (project.path in [':messaging:messaging-kafka',\n ':messaging:messaging-rabbit',\n ':messaging:messaging-testkit']) {\n```\n\n그 아래에서 `spotbugsJmh` 와 `checkstyleJmh` 를 **끈다**.\n\n```groovy\n// src/build.gradle:528-535\n// JMH's annotation processor emits the generated harness into this source set, and its\n// generated code trips DLS_DEAD_LOCAL_STORE by design (the dead stores are how it defeats\n// dead-code elimination). … The benchmarks themselves are still compiled, which is what\n// catches a real breakage.\ntasks.named('spotbugsJmh') { enabled = false }\ntasks.named('checkstyleJmh') { enabled = false }\n```\n\n마지막 문장(\"still compiled\")이 참인지가 갈림길이다. 그 두 태스크가 `check → compileJmhJava` 로 가는 유일한 경로이기 때문이다. 실측했다(`EVD-298`):\n\n```\n./gradlew :messaging:messaging-testkit:build --dry-run\n :messaging:messaging-testkit:compileJmhJava SKIPPED\n :messaging:messaging-testkit:jmhClasses SKIPPED\n :messaging:messaging-testkit:checkstyleJmh SKIPPED\n :messaging:messaging-testkit:spotbugsJmh SKIPPED\n```\n\n**참이다.** Gradle 의 `enabled = false` 는 태스크 액션만 건너뛰고 의존성 그래프는 유지하므로, 꺼진 `checkstyleJmh`/`spotbugsJmh` 가 여전히 `compileJmhJava` 를 끌고 들어온다. 벤치마크는 매 빌드에서 컴파일되고 실행만 온디맨드다. 반직관적이라 증거로 남겼다.\n\n`compileJmhJava` 에서 ErrorProne 을 끄고 `-Werror` 를 제거하는 이유도 명시적이다: \"ErrorProne's -Werror would reject JMH's generated sources, which the platform does not own and cannot fix.\"\n\n`jmh` 태스크는 `JavaExec` 로 `org.openjdk.jmh.Main` 을 부른다(`:536-541`).\n\n`EnvelopeCodecBenchmark` 가 무엇을 재는지에 대한 판단도 적혀 있다.\n\n```java\n// EnvelopeCodecBenchmark.java:31-41\n/**\n * Measures the per-message cost the platform adds before any broker is involved.\n *\n *
This is the number the platform is accountable for. Broker latency dominates any real publish\n * and varies with the network, so measuring it would tell you about the test environment; …\n *\n *
Header validation is benchmarked separately from envelope construction because they scale\n * differently: construction is constant, while validation is linear in the header count …\n */\n```\n\n4개 벤치마크: `generateMessageId`(UuidV7), `validateFewHeaders`(3개), `validateManyHeaders`(32개), `buildEnvelope`. 헤더 맵을 `@Setup` 에서 미리 만들어 \"맵 생성이 아니라 검증을 잰다\"는 것을 보장한다.\n\n---\n" }, "previous_section": { "heading": { "line": 40142, "level": 4, "text": "10. 테스트 레인과 실제 증명 범위" }, "start_line": 40142, "end_line": 40171, "text": "#### 10. 테스트 레인과 실제 증명 범위\n\n`EVD-301`: `./gradlew :messaging:messaging-testkit:test --rerun-tasks` → **44 tests, 0 failures, 0 errors, 0 skipped**.\n\n| 클래스 | 수 | 증명 대상 |\n|---|---:|---|\n| `CompatibilityMatrixTest` | 11 | 등급 규칙, 계약 크기 잠금, 미등록 어댑터 거절 |\n| `CrossBrokerContractSuite` | 10 | 증거→커버리지 변환, 기대 불일치 거절, 시나리오 불변식 |\n| `CertifiedEvidenceTest` | 8 | 매니페스트 원본성, 이미지/테스트ID 형식, gap 명명, 직렬화 왕복·거절 |\n| `MessagingDocumentationContractTest` | 8 | `docs/messaging/*.md` 9개 존재·내용·등급 일치 |\n| `InMemoryHarnessContractTest$Contract` | 7 | 공유 계약 7개 |\n\n**이 레인이 증명하는 것과 증명하지 않는 것의 경계가 이 리프의 핵심이다.**\n\n증명한다: 매니페스트를 읽는 코드가 옳다. 등급이 매니페스트에서 파생된다. 문서가 등급과 일치한다. 계약이 7개다. 계약 7개가 결정론적 하니스에서 통과한다.\n\n증명하지 않는다: **매니페스트에 든 4줄이 진짜 실행에서 나왔다는 것.** 그것은 `messaging-kafka:verifyMessagingCertificationEvidence` 만 증명하고, 그 레인은 `test` 에서 제외되어 있으며 Docker 를 요구한다. 이 세션에서 실행하지 않았다(§16).\n\n`MessagingDocumentationContractTest` 의 자기 제한이 좋다.\n\n```java\n// MessagingDocumentationContractTest.java:18-20\n *
The assertions are deliberately narrow: they check the claims a reader would act on, not\n * prose. Asserting on wording would make every edit a test failure and the check would be deleted.\n```\n\n문서 검사가 삭제당하지 않도록 검사 범위를 스스로 좁혔다. 그리고 `noEnumConstantTheDocsDenyActuallyExists` 는 방향이 반대다 — 문서가 \"없다\"고 한 것(`EXACTLY_ONCE`, `GLOBAL`)이 실제로 enum 에 없는지를 확인한다. 문서의 **부정 주장**을 코드로 검증하는 것은 드문 패턴이다.\n\n---\n" }, "next_section": { "heading": { "line": 40234, "level": 4, "text": "12. 실제 사용 여부와 negative-space probes" }, "start_line": 40234, "end_line": 40387, "text": "#### 12. 실제 사용 여부와 negative-space probes\n\n##### 12.1 Public surface reachability\n\n**방법 주의.** 참조 계수는 단어 검색이 아니라 `import dev.caskeleton.messaging.testkit` 및 타입별 `git grep` 으로 셌다. 이 리프의 타입 이름(`CompatibilityMatrix`, `BrokerFailureMatrix` 등)은 저장소 내 동명 클래스가 없어 충돌은 없었으나, `isComplete`/`reset` 같은 **메서드 이름은 충돌이 심하다** — `git grep \"isComplete\"` 는 10건을 내지만 9건이 fileserver/websocket 의 무관한 클래스다(`EVD-299`, `EVD-300`). 메서드 단위 판정은 전부 소유 타입을 확인한 뒤 세었다.\n\n| 타입/멤버 | leaf 밖 참조 | 판정 |\n|---|---:|---|\n| `MessagingAdapterContract` | 2 (kafka, rabbit `extends`) | 사용됨 |\n| `MessagingAdapterHarness` | 2 (`implements`) | 사용됨 |\n| `ContractMessage` / `ContractAssertions` / `ObservedDelivery` / `HandleOutcome` | 2씩 | 사용됨 |\n| `DockerAvailability` | 4 모듈 | 사용됨 |\n| `NetworkFaultScenario`, `BrokerCertificationEvidence` | 1 (kafka 인증 IT) | 사용됨 |\n| `CertifiedEvidence` | 1 | 사용됨 |\n| `CompatibilityMatrix` | **0** (leaf 내부 테스트만) | leaf-local |\n| `BrokerFailureMatrix` | **0** (leaf 내부 테스트만) | leaf-local |\n| `FaultController.rejectPublish()` | **호출 0건** | §17 P2 |\n| `FaultController.reset()` | **호출 0건** | §17 P2 |\n| `BrokerFailureMatrix.adapters()` | **호출 0건** | §17 P3 |\n| `BrokerFailureMatrix.isComplete(...)` | 호출 1건, Experimental 에만 | §12.4 |\n\n`CompatibilityMatrix`/`BrokerFailureMatrix` 가 leaf 밖 참조 0인 것은 결함이 아니다. 이 둘의 소비자는 문서와 릴리스 판정이고, 그 판정은 이 리프의 테스트에서 이뤄지도록 설계되어 있다.\n\n`FaultController` 의 두 미사용 메서드는 다르다(`EVD-299`).\n\n```\ndropPublishConfirmation() : 호출 1건 (MessagingAdapterContract:40)\ndropSettlementConfirmation(): 호출 1건 (MessagingAdapterContract:53)\nfailDeadLetterPublish() : 호출 1건 (MessagingAdapterContract:104)\nrejectPublish() : 호출 0건\nreset() : 호출 0건\n```\n\n인터페이스 5개 중 3개만 계약이 쓴다. 나머지 2개는 **구현이 3벌 강제되면서 아무도 부르지 않는다**.\n\n##### 12.2 Conditional sibling comparison\n\n같은 저장소에 \"증거 기반 등급\" 을 하는 형제가 하나 더 있다.\n\n```\nsrc/adapter/outbound/persistence-mongo/src/test/java/.../performance/MongoReleaseEvidenceTest.java\n MongoFailoverScenario.all().forEach(scenario -> gate.record(scenario, true));\n```\n\nMongo 쪽은 `gate.record(scenario, true)` 를 테스트가 직접 호출한다 — 즉 **테스트가 증거를 선언한다**. messaging 쪽은 매니페스트 파일이 증거를 나르고 테스트는 읽기만 한다. `CertifiedEvidence` 의 javadoc 이 고쳤다고 말하는 바로 그 형태가 Mongo 쪽에는 아직 남아 있다. 이는 이 리프의 결함이 아니라 **같은 교훈이 아직 전파되지 않은 곳**이며, family 문서에서 다룰 대비다.\n\nmessaging 내부에서 `DockerAvailability` 를 쓰는 4개 모듈과 인증 레인의 관계도 대비된다: 전자는 없으면 skip, 후자는 가드 없이 실패 — §6 에 근거 인용.\n\n##### 12.3 Duplicate mechanism sweep\n\n**(a) `Faults` 내부클래스 3중복 — 바이트 동일.** (`EVD-299`)\n\n```\nKafkaContractHarness.java : 57줄 sha256[0:16]=3028b4591144fe86\nRabbitContractHarness.java : 57줄 sha256[0:16]=3028b4591144fe86\nInMemoryMessagingHarness.java: 57줄 sha256[0:16]=3028b4591144fe86\ndiff kafka vs rabbit -> IDENTICAL\ndiff kafka vs inmemory -> IDENTICAL\n```\n\n`private static final class Faults implements FaultController` 57줄이 3개 모듈에 완전히 동일하게 존재한다. 총 171줄. 4개 불리언 필드 + 5개 오버라이드 + 4개 consume/query 메서드. 이 리프의 `src/main` 에 `DefaultFaultController` 하나만 두면 3벌이 1벌이 된다. 세 하니스가 `faults` 필드 타입만 공유하면 되므로 API 변경도 필요 없다.\n\n**(b) 1 MiB 한도 리터럴 8중복.** `PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576` 이 있는데도 같은 값이 리터럴로 다시 선언된다.\n\n```\nmessaging-policy/PayloadPolicy.java:17 DEFAULT_MAX_BYTES = 1_048_576 <- 정본\nmessaging-schema-api/RawBytesMessageCodec.java:21 DEFAULT_MAX_BYTES = 1_048_576\nmessaging-schema-json/JacksonMessageCodec.java:42 DEFAULT_MAX_BYTES = 1_048_576\nmessaging-schema-avro/AvroMessageCodec.java:46 DEFAULT_MAX_BYTES = 1_048_576\nmessaging-schema-protobuf/ProtobufMessageCodec.java:35 DEFAULT_MAX_BYTES = 1_048_576\nmessaging-claim-check/…RetentionValidatorTest.java:47 PORTABLE_PAYLOAD_LIMIT_BYTES\nmessaging-rabbit/RabbitContractHarness.java:40 MAX_PAYLOAD_BYTES\nmessaging-testkit/InMemoryMessagingHarness.java:31 MAX_PAYLOAD_BYTES <- 이 리프\nmessaging-testkit/ContractMessage.java:50 new byte[1_048_577] <- 이 리프\nmessaging-spring-boot-starter/DestinationSettings.java:175 @DefaultValue(\"1048576\")\n```\n\n`messaging-testkit` 은 `api project(':messaging:messaging-policy')` 를 선언하고 있으므로 `PayloadPolicy.DEFAULT_MAX_BYTES` 를 그냥 참조할 수 있다. §12.4 의 \"policy 미사용\" 과 합치면, 유일하게 policy 를 써야 할 자리에서 쓰지 않고 있는 셈이다.\n\n**(c) `hasLiveBrokerCertification` 대조 테스트의 항등식.**\n\n```java\n// CompatibilityMatrixTest.java:48-60 aCertificationClaimCannotBeMadeWithoutEvidence\nassertThat(entry.hasLiveBrokerCertification())\n .isEqualTo(BrokerFailureMatrix.from(CertifiedEvidence.recorded()).hasLiveBrokerCoverage(entry.adapter()));\n```\n\n`Entry.hasLiveBrokerCertification()` 의 본문이 정확히 우변과 같다(`CompatibilityMatrix.java:60-62`). 이 단언은 항상 참인 항등식이며, 어떤 회귀도 잡지 못한다. 같은 파일의 `everyStableAdapterIsCertifiedAgainstALiveBroker`(`:97-109`)와 `noExperimentalAdapterClaimsLiveBrokerCertification`(`:111-116`)이 실질 검사를 하고 있어 커버리지 손실은 없지만, 이름이 약속하는 것(\"증거 없이 인증 주장 불가\")을 이 테스트 자체는 검사하지 않는다.\n\n##### 12.4 Documentation / measured-count drift\n\n**(a) `BrokerFailureMatrix` 클래스 javadoc 이 강제되지 않는 규칙을 선언한다.** (`EVD-300`)\n\n```java\n// BrokerFailureMatrix.java:18-20\n *
A Stable adapter must cover every scenario. That rule is enforced by a test rather than\n * documented, because a promotion to Stable is exactly the moment the gap would otherwise be\n * overlooked.\n```\n\n측정:\n\n```\ngit grep -n \"isComplete\" -- src (messaging-testkit 범위)\n BrokerFailureMatrix.java:95 public boolean isComplete(String adapter) {\n CrossBrokerContractSuite.java:110 assertThat(matrix.isComplete(\"messaging-pulsar-experimental\"))\n```\n\n`isComplete` 의 호출부는 1곳이고 그것은 **Experimental** 어댑터가 불완전함을 단언한다. Stable 어댑터에 `isComplete` 를 거는 테스트는 없다.\n\n그리고 실제로 Stable 인 `messaging-kafka` 는 gap 을 가진 채 통과한다 — 그 사실이 같은 모듈에서 **명시적으로 단언되어 있다**.\n\n```java\n// CertifiedEvidenceTest.java:52-55\nassertThat(CertifiedEvidence.knownGaps(\"messaging-kafka\"))\n .as(\"a Kafka producer buffers before it learns a connection exists, so this stays unproven\")\n .contains(NetworkFaultScenario.CONNECTION_REFUSED);\n```\n\n코드는 \"정직한 gap 열거\"로 바뀌었고 그 결정이 테스트 본문 주석에 남아 있다.\n\n```java\n// CrossBrokerContractSuite.java:44-47\nvoid everyStableAdapterCoversEveryFaultScenario() {\n // The gaps are named rather than asserted empty. A Stable adapter with unrun scenarios is the\n // current, honest state; asserting emptiness here would only reinstate the self-declaration.\n```\n\n**바뀌지 않은 것은 두 가지다**: `BrokerFailureMatrix` 의 클래스 javadoc 과, 저 테스트 메서드 이름(`everyStableAdapterCoversEveryFaultScenario` — 본문은 covers 를 검사하지 않는다). 이 리프의 나머지 javadoc 들이 자기 이력을 정확히 갱신해 온 것과 대비되어 눈에 띈다.\n\n**(b) 선언된 project 의존 4개 중 2개가 import 0건.**\n\n```\nmessaging-core-api -> 사용 O\nmessaging-schema-api -> 사용 O (EncodedMessage)\nmessaging-policy -> import 0건\nmessaging-transport-spi -> import 0건\n```\n\n`messaging-spring-cloud-stream-bridge`, `messaging-kafka-share-experimental` 에서 이미 본 것과 같은 형태다. 다만 여기는 §12.3(b) 때문에 성격이 다르다 — policy 를 **안 쓰는 게 아니라 써야 하는데 리터럴로 우회**하고 있다.\n\n**(c) 증거의 커밋이 현재 트리가 아니다.**\n\n```\n현재 HEAD : 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916\n매니페스트의 gitCommit : e98b56eb03ecab588c21fd1e7dbcaa493c1d8645 (히스토리에 존재)\n```\n\n이는 결함이 아니다. 게이트가 `gitCommit`/`observedAt` 을 비교에서 제외하는 것이 명시적 설계이며 그 이유가 주석에 있다. 다만 `BrokerCertificationEvidence` javadoc 이 \"the commit are here because 'certified' is a claim about … a specific source tree; without them the evidence cannot be checked against anything later\" 라고 쓴 것에 비해, 실제로 그 필드를 **읽어서 무언가를 판정하는 코드는 없다**. 기록은 되고 활용은 되지 않는다.\n\n**(d) 지원 문서 9개 존재·내용 검사는 통과.** `MessagingDocumentationContractTest` 8건 전부 통과(`EVD-301`). 단, 이 검사는 `docs/messaging/support-matrix.md` 의 **등급 표기**만 본다. 같은 문서 23줄의 `runtime_memberships` 관련 서술 드리프트는 이 검사의 사정권 밖이며 §A19 에서 다룬다.\n\n---\n" }, "context_range": { "start_line": 40142, "end_line": 40387 }, "context_lines": [ { "line": 40142, "text": "#### 10. 테스트 레인과 실제 증명 범위" }, { "line": 40143, "text": "" }, { "line": 40144, "text": "`EVD-301`: `./gradlew :messaging:messaging-testkit:test --rerun-tasks` → **44 tests, 0 failures, 0 errors, 0 skipped**." }, { "line": 40145, "text": "" }, { "line": 40146, "text": "| 클래스 | 수 | 증명 대상 |" }, { "line": 40147, "text": "|---|---:|---|" }, { "line": 40148, "text": "| `CompatibilityMatrixTest` | 11 | 등급 규칙, 계약 크기 잠금, 미등록 어댑터 거절 |" }, { "line": 40149, "text": "| `CrossBrokerContractSuite` | 10 | 증거→커버리지 변환, 기대 불일치 거절, 시나리오 불변식 |" }, { "line": 40150, "text": "| `CertifiedEvidenceTest` | 8 | 매니페스트 원본성, 이미지/테스트ID 형식, gap 명명, 직렬화 왕복·거절 |" }, { "line": 40151, "text": "| `MessagingDocumentationContractTest` | 8 | `docs/messaging/*.md` 9개 존재·내용·등급 일치 |" }, { "line": 40152, "text": "| `InMemoryHarnessContractTest$Contract` | 7 | 공유 계약 7개 |" }, { "line": 40153, "text": "" }, { "line": 40154, "text": "**이 레인이 증명하는 것과 증명하지 않는 것의 경계가 이 리프의 핵심이다.**" }, { "line": 40155, "text": "" }, { "line": 40156, "text": "증명한다: 매니페스트를 읽는 코드가 옳다. 등급이 매니페스트에서 파생된다. 문서가 등급과 일치한다. 계약이 7개다. 계약 7개가 결정론적 하니스에서 통과한다." }, { "line": 40157, "text": "" }, { "line": 40158, "text": "증명하지 않는다: **매니페스트에 든 4줄이 진짜 실행에서 나왔다는 것.** 그것은 `messaging-kafka:verifyMessagingCertificationEvidence` 만 증명하고, 그 레인은 `test` 에서 제외되어 있으며 Docker 를 요구한다. 이 세션에서 실행하지 않았다(§16)." }, { "line": 40159, "text": "" }, { "line": 40160, "text": "`MessagingDocumentationContractTest` 의 자기 제한이 좋다." }, { "line": 40161, "text": "" }, { "line": 40162, "text": "```java" }, { "line": 40163, "text": "// MessagingDocumentationContractTest.java:18-20" }, { "line": 40164, "text": " *
The assertions are deliberately narrow: they check the claims a reader would act on, not" }, { "line": 40165, "text": " * prose. Asserting on wording would make every edit a test failure and the check would be deleted." }, { "line": 40166, "text": "```" }, { "line": 40167, "text": "" }, { "line": 40168, "text": "문서 검사가 삭제당하지 않도록 검사 범위를 스스로 좁혔다. 그리고 `noEnumConstantTheDocsDenyActuallyExists` 는 방향이 반대다 — 문서가 \"없다\"고 한 것(`EXACTLY_ONCE`, `GLOBAL`)이 실제로 enum 에 없는지를 확인한다. 문서의 **부정 주장**을 코드로 검증하는 것은 드문 패턴이다." }, { "line": 40169, "text": "" }, { "line": 40170, "text": "---" }, { "line": 40171, "text": "" }, { "line": 40172, "text": "#### 11. 빌드/ArchUnit/CI 강제 지점" }, { "line": 40173, "text": "" }, { "line": 40174, "text": "`jmh` 소스셋이 루트에서 정확히 3개 리프에만 부여된다." }, { "line": 40175, "text": "" }, { "line": 40176, "text": "```groovy" }, { "line": 40177, "text": "// src/build.gradle:500-508" }, { "line": 40178, "text": "// The three messaging leaves that carry JMH benchmarks get a `jmh` source set. It is a source" }, { "line": 40179, "text": "// set rather than a plugin because the benchmarks are compiled and reviewed on every build but" }, { "line": 40180, "text": "// only *run* on demand: a benchmark that stops compiling is a defect, while a benchmark that" }, { "line": 40181, "text": "// runs in CI is a flaky test measuring the build agent." }, { "line": 40182, "text": "if (project.path in [':messaging:messaging-kafka'," }, { "line": 40183, "text": " ':messaging:messaging-rabbit'," }, { "line": 40184, "text": " ':messaging:messaging-testkit']) {" }, { "line": 40185, "text": "```" }, { "line": 40186, "text": "" }, { "line": 40187, "text": "그 아래에서 `spotbugsJmh` 와 `checkstyleJmh` 를 **끈다**." }, { "line": 40188, "text": "" }, { "line": 40189, "text": "```groovy" }, { "line": 40190, "text": "// src/build.gradle:528-535" }, { "line": 40191, "text": "// JMH's annotation processor emits the generated harness into this source set, and its" }, { "line": 40192, "text": "// generated code trips DLS_DEAD_LOCAL_STORE by design (the dead stores are how it defeats" }, { "line": 40193, "text": "// dead-code elimination). … The benchmarks themselves are still compiled, which is what" }, { "line": 40194, "text": "// catches a real breakage." }, { "line": 40195, "text": "tasks.named('spotbugsJmh') { enabled = false }" }, { "line": 40196, "text": "tasks.named('checkstyleJmh') { enabled = false }" }, { "line": 40197, "text": "```" }, { "line": 40198, "text": "" }, { "line": 40199, "text": "마지막 문장(\"still compiled\")이 참인지가 갈림길이다. 그 두 태스크가 `check → compileJmhJava` 로 가는 유일한 경로이기 때문이다. 실측했다(`EVD-298`):" }, { "line": 40200, "text": "" }, { "line": 40201, "text": "```" }, { "line": 40202, "text": "./gradlew :messaging:messaging-testkit:build --dry-run" }, { "line": 40203, "text": " :messaging:messaging-testkit:compileJmhJava SKIPPED" }, { "line": 40204, "text": " :messaging:messaging-testkit:jmhClasses SKIPPED" }, { "line": 40205, "text": " :messaging:messaging-testkit:checkstyleJmh SKIPPED" }, { "line": 40206, "text": " :messaging:messaging-testkit:spotbugsJmh SKIPPED" }, { "line": 40207, "text": "```" }, { "line": 40208, "text": "" }, { "line": 40209, "text": "**참이다.** Gradle 의 `enabled = false` 는 태스크 액션만 건너뛰고 의존성 그래프는 유지하므로, 꺼진 `checkstyleJmh`/`spotbugsJmh` 가 여전히 `compileJmhJava` 를 끌고 들어온다. 벤치마크는 매 빌드에서 컴파일되고 실행만 온디맨드다. 반직관적이라 증거로 남겼다." }, { "line": 40210, "text": "" }, { "line": 40211, "text": "`compileJmhJava` 에서 ErrorProne 을 끄고 `-Werror` 를 제거하는 이유도 명시적이다: \"ErrorProne's -Werror would reject JMH's generated sources, which the platform does not own and cannot fix.\"" }, { "line": 40212, "text": "" }, { "line": 40213, "text": "`jmh` 태스크는 `JavaExec` 로 `org.openjdk.jmh.Main` 을 부른다(`:536-541`)." }, { "line": 40214, "text": "" }, { "line": 40215, "text": "`EnvelopeCodecBenchmark` 가 무엇을 재는지에 대한 판단도 적혀 있다." }, { "line": 40216, "text": "" }, { "line": 40217, "text": "```java" }, { "line": 40218, "text": "// EnvelopeCodecBenchmark.java:31-41" }, { "line": 40219, "text": "/**" }, { "line": 40220, "text": " * Measures the per-message cost the platform adds before any broker is involved." }, { "line": 40221, "text": " *" }, { "line": 40222, "text": " *
This is the number the platform is accountable for. Broker latency dominates any real publish" }, { "line": 40223, "text": " * and varies with the network, so measuring it would tell you about the test environment; …" }, { "line": 40224, "text": " *" }, { "line": 40225, "text": " *
Header validation is benchmarked separately from envelope construction because they scale" }, { "line": 40226, "text": " * differently: construction is constant, while validation is linear in the header count …" }, { "line": 40227, "text": " */" }, { "line": 40228, "text": "```" }, { "line": 40229, "text": "" }, { "line": 40230, "text": "4개 벤치마크: `generateMessageId`(UuidV7), `validateFewHeaders`(3개), `validateManyHeaders`(32개), `buildEnvelope`. 헤더 맵을 `@Setup` 에서 미리 만들어 \"맵 생성이 아니라 검증을 잰다\"는 것을 보장한다." }, { "line": 40231, "text": "" }, { "line": 40232, "text": "---" }, { "line": 40233, "text": "" }, { "line": 40234, "text": "#### 12. 실제 사용 여부와 negative-space probes" }, { "line": 40235, "text": "" }, { "line": 40236, "text": "##### 12.1 Public surface reachability" }, { "line": 40237, "text": "" }, { "line": 40238, "text": "**방법 주의.** 참조 계수는 단어 검색이 아니라 `import dev.caskeleton.messaging.testkit` 및 타입별 `git grep` 으로 셌다. 이 리프의 타입 이름(`CompatibilityMatrix`, `BrokerFailureMatrix` 등)은 저장소 내 동명 클래스가 없어 충돌은 없었으나, `isComplete`/`reset` 같은 **메서드 이름은 충돌이 심하다** — `git grep \"isComplete\"` 는 10건을 내지만 9건이 fileserver/websocket 의 무관한 클래스다(`EVD-299`, `EVD-300`). 메서드 단위 판정은 전부 소유 타입을 확인한 뒤 세었다." }, { "line": 40239, "text": "" }, { "line": 40240, "text": "| 타입/멤버 | leaf 밖 참조 | 판정 |" }, { "line": 40241, "text": "|---|---:|---|" }, { "line": 40242, "text": "| `MessagingAdapterContract` | 2 (kafka, rabbit `extends`) | 사용됨 |" }, { "line": 40243, "text": "| `MessagingAdapterHarness` | 2 (`implements`) | 사용됨 |" }, { "line": 40244, "text": "| `ContractMessage` / `ContractAssertions` / `ObservedDelivery` / `HandleOutcome` | 2씩 | 사용됨 |" }, { "line": 40245, "text": "| `DockerAvailability` | 4 모듈 | 사용됨 |" }, { "line": 40246, "text": "| `NetworkFaultScenario`, `BrokerCertificationEvidence` | 1 (kafka 인증 IT) | 사용됨 |" }, { "line": 40247, "text": "| `CertifiedEvidence` | 1 | 사용됨 |" }, { "line": 40248, "text": "| `CompatibilityMatrix` | **0** (leaf 내부 테스트만) | leaf-local |" }, { "line": 40249, "text": "| `BrokerFailureMatrix` | **0** (leaf 내부 테스트만) | leaf-local |" }, { "line": 40250, "text": "| `FaultController.rejectPublish()` | **호출 0건** | §17 P2 |" }, { "line": 40251, "text": "| `FaultController.reset()` | **호출 0건** | §17 P2 |" }, { "line": 40252, "text": "| `BrokerFailureMatrix.adapters()` | **호출 0건** | §17 P3 |" }, { "line": 40253, "text": "| `BrokerFailureMatrix.isComplete(...)` | 호출 1건, Experimental 에만 | §12.4 |" }, { "line": 40254, "text": "" }, { "line": 40255, "text": "`CompatibilityMatrix`/`BrokerFailureMatrix` 가 leaf 밖 참조 0인 것은 결함이 아니다. 이 둘의 소비자는 문서와 릴리스 판정이고, 그 판정은 이 리프의 테스트에서 이뤄지도록 설계되어 있다." }, { "line": 40256, "text": "" }, { "line": 40257, "text": "`FaultController` 의 두 미사용 메서드는 다르다(`EVD-299`)." }, { "line": 40258, "text": "" }, { "line": 40259, "text": "```" }, { "line": 40260, "text": "dropPublishConfirmation() : 호출 1건 (MessagingAdapterContract:40)" }, { "line": 40261, "text": "dropSettlementConfirmation(): 호출 1건 (MessagingAdapterContract:53)" }, { "line": 40262, "text": "failDeadLetterPublish() : 호출 1건 (MessagingAdapterContract:104)" }, { "line": 40263, "text": "rejectPublish() : 호출 0건" }, { "line": 40264, "text": "reset() : 호출 0건" }, { "line": 40265, "text": "```" }, { "line": 40266, "text": "" }, { "line": 40267, "text": "인터페이스 5개 중 3개만 계약이 쓴다. 나머지 2개는 **구현이 3벌 강제되면서 아무도 부르지 않는다**." }, { "line": 40268, "text": "" }, { "line": 40269, "text": "##### 12.2 Conditional sibling comparison" }, { "line": 40270, "text": "" }, { "line": 40271, "text": "같은 저장소에 \"증거 기반 등급\" 을 하는 형제가 하나 더 있다." }, { "line": 40272, "text": "" }, { "line": 40273, "text": "```" }, { "line": 40274, "text": "src/adapter/outbound/persistence-mongo/src/test/java/.../performance/MongoReleaseEvidenceTest.java" }, { "line": 40275, "text": " MongoFailoverScenario.all().forEach(scenario -> gate.record(scenario, true));" }, { "line": 40276, "text": "```" }, { "line": 40277, "text": "" }, { "line": 40278, "text": "Mongo 쪽은 `gate.record(scenario, true)` 를 테스트가 직접 호출한다 — 즉 **테스트가 증거를 선언한다**. messaging 쪽은 매니페스트 파일이 증거를 나르고 테스트는 읽기만 한다. `CertifiedEvidence` 의 javadoc 이 고쳤다고 말하는 바로 그 형태가 Mongo 쪽에는 아직 남아 있다. 이는 이 리프의 결함이 아니라 **같은 교훈이 아직 전파되지 않은 곳**이며, family 문서에서 다룰 대비다." }, { "line": 40279, "text": "" }, { "line": 40280, "text": "messaging 내부에서 `DockerAvailability` 를 쓰는 4개 모듈과 인증 레인의 관계도 대비된다: 전자는 없으면 skip, 후자는 가드 없이 실패 — §6 에 근거 인용." }, { "line": 40281, "text": "" }, { "line": 40282, "text": "##### 12.3 Duplicate mechanism sweep" }, { "line": 40283, "text": "" }, { "line": 40284, "text": "**(a) `Faults` 내부클래스 3중복 — 바이트 동일.** (`EVD-299`)" }, { "line": 40285, "text": "" }, { "line": 40286, "text": "```" }, { "line": 40287, "text": "KafkaContractHarness.java : 57줄 sha256[0:16]=3028b4591144fe86" }, { "line": 40288, "text": "RabbitContractHarness.java : 57줄 sha256[0:16]=3028b4591144fe86" }, { "line": 40289, "text": "InMemoryMessagingHarness.java: 57줄 sha256[0:16]=3028b4591144fe86" }, { "line": 40290, "text": "diff kafka vs rabbit -> IDENTICAL" }, { "line": 40291, "text": "diff kafka vs inmemory -> IDENTICAL" }, { "line": 40292, "text": "```" }, { "line": 40293, "text": "" }, { "line": 40294, "text": "`private static final class Faults implements FaultController` 57줄이 3개 모듈에 완전히 동일하게 존재한다. 총 171줄. 4개 불리언 필드 + 5개 오버라이드 + 4개 consume/query 메서드. 이 리프의 `src/main` 에 `DefaultFaultController` 하나만 두면 3벌이 1벌이 된다. 세 하니스가 `faults` 필드 타입만 공유하면 되므로 API 변경도 필요 없다." }, { "line": 40295, "text": "" }, { "line": 40296, "text": "**(b) 1 MiB 한도 리터럴 8중복.** `PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576` 이 있는데도 같은 값이 리터럴로 다시 선언된다." }, { "line": 40297, "text": "" }, { "line": 40298, "text": "```" }, { "line": 40299, "text": "messaging-policy/PayloadPolicy.java:17 DEFAULT_MAX_BYTES = 1_048_576 <- 정본" }, { "line": 40300, "text": "messaging-schema-api/RawBytesMessageCodec.java:21 DEFAULT_MAX_BYTES = 1_048_576" }, { "line": 40301, "text": "messaging-schema-json/JacksonMessageCodec.java:42 DEFAULT_MAX_BYTES = 1_048_576" }, { "line": 40302, "text": "messaging-schema-avro/AvroMessageCodec.java:46 DEFAULT_MAX_BYTES = 1_048_576" }, { "line": 40303, "text": "messaging-schema-protobuf/ProtobufMessageCodec.java:35 DEFAULT_MAX_BYTES = 1_048_576" }, { "line": 40304, "text": "messaging-claim-check/…RetentionValidatorTest.java:47 PORTABLE_PAYLOAD_LIMIT_BYTES" }, { "line": 40305, "text": "messaging-rabbit/RabbitContractHarness.java:40 MAX_PAYLOAD_BYTES" }, { "line": 40306, "text": "messaging-testkit/InMemoryMessagingHarness.java:31 MAX_PAYLOAD_BYTES <- 이 리프" }, { "line": 40307, "text": "messaging-testkit/ContractMessage.java:50 new byte[1_048_577] <- 이 리프" }, { "line": 40308, "text": "messaging-spring-boot-starter/DestinationSettings.java:175 @DefaultValue(\"1048576\")" }, { "line": 40309, "text": "```" }, { "line": 40310, "text": "" }, { "line": 40311, "text": "`messaging-testkit` 은 `api project(':messaging:messaging-policy')` 를 선언하고 있으므로 `PayloadPolicy.DEFAULT_MAX_BYTES` 를 그냥 참조할 수 있다. §12.4 의 \"policy 미사용\" 과 합치면, 유일하게 policy 를 써야 할 자리에서 쓰지 않고 있는 셈이다." }, { "line": 40312, "text": "" }, { "line": 40313, "text": "**(c) `hasLiveBrokerCertification` 대조 테스트의 항등식.**" }, { "line": 40314, "text": "" }, { "line": 40315, "text": "```java" }, { "line": 40316, "text": "// CompatibilityMatrixTest.java:48-60 aCertificationClaimCannotBeMadeWithoutEvidence" }, { "line": 40317, "text": "assertThat(entry.hasLiveBrokerCertification())" }, { "line": 40318, "text": " .isEqualTo(BrokerFailureMatrix.from(CertifiedEvidence.recorded()).hasLiveBrokerCoverage(entry.adapter()));" }, { "line": 40319, "text": "```" }, { "line": 40320, "text": "" }, { "line": 40321, "text": "`Entry.hasLiveBrokerCertification()` 의 본문이 정확히 우변과 같다(`CompatibilityMatrix.java:60-62`). 이 단언은 항상 참인 항등식이며, 어떤 회귀도 잡지 못한다. 같은 파일의 `everyStableAdapterIsCertifiedAgainstALiveBroker`(`:97-109`)와 `noExperimentalAdapterClaimsLiveBrokerCertification`(`:111-116`)이 실질 검사를 하고 있어 커버리지 손실은 없지만, 이름이 약속하는 것(\"증거 없이 인증 주장 불가\")을 이 테스트 자체는 검사하지 않는다." }, { "line": 40322, "text": "" }, { "line": 40323, "text": "##### 12.4 Documentation / measured-count drift" }, { "line": 40324, "text": "" }, { "line": 40325, "text": "**(a) `BrokerFailureMatrix` 클래스 javadoc 이 강제되지 않는 규칙을 선언한다.** (`EVD-300`)" }, { "line": 40326, "text": "" }, { "line": 40327, "text": "```java" }, { "line": 40328, "text": "// BrokerFailureMatrix.java:18-20" }, { "line": 40329, "text": " *
A Stable adapter must cover every scenario. That rule is enforced by a test rather than" }, { "line": 40330, "text": " * documented, because a promotion to Stable is exactly the moment the gap would otherwise be" }, { "line": 40331, "text": " * overlooked." }, { "line": 40332, "text": "```" }, { "line": 40333, "text": "" }, { "line": 40334, "text": "측정:" }, { "line": 40335, "text": "" }, { "line": 40336, "text": "```" }, { "line": 40337, "text": "git grep -n \"isComplete\" -- src (messaging-testkit 범위)" }, { "line": 40338, "text": " BrokerFailureMatrix.java:95 public boolean isComplete(String adapter) {" }, { "line": 40339, "text": " CrossBrokerContractSuite.java:110 assertThat(matrix.isComplete(\"messaging-pulsar-experimental\"))" }, { "line": 40340, "text": "```" }, { "line": 40341, "text": "" }, { "line": 40342, "text": "`isComplete` 의 호출부는 1곳이고 그것은 **Experimental** 어댑터가 불완전함을 단언한다. Stable 어댑터에 `isComplete` 를 거는 테스트는 없다." }, { "line": 40343, "text": "" }, { "line": 40344, "text": "그리고 실제로 Stable 인 `messaging-kafka` 는 gap 을 가진 채 통과한다 — 그 사실이 같은 모듈에서 **명시적으로 단언되어 있다**." }, { "line": 40345, "text": "" }, { "line": 40346, "text": "```java" }, { "line": 40347, "text": "// CertifiedEvidenceTest.java:52-55" }, { "line": 40348, "text": "assertThat(CertifiedEvidence.knownGaps(\"messaging-kafka\"))" }, { "line": 40349, "text": " .as(\"a Kafka producer buffers before it learns a connection exists, so this stays unproven\")" }, { "line": 40350, "text": " .contains(NetworkFaultScenario.CONNECTION_REFUSED);" }, { "line": 40351, "text": "```" }, { "line": 40352, "text": "" }, { "line": 40353, "text": "코드는 \"정직한 gap 열거\"로 바뀌었고 그 결정이 테스트 본문 주석에 남아 있다." }, { "line": 40354, "text": "" }, { "line": 40355, "text": "```java" }, { "line": 40356, "text": "// CrossBrokerContractSuite.java:44-47" }, { "line": 40357, "text": "void everyStableAdapterCoversEveryFaultScenario() {" }, { "line": 40358, "text": " // The gaps are named rather than asserted empty. A Stable adapter with unrun scenarios is the" }, { "line": 40359, "text": " // current, honest state; asserting emptiness here would only reinstate the self-declaration." }, { "line": 40360, "text": "```" }, { "line": 40361, "text": "" }, { "line": 40362, "text": "**바뀌지 않은 것은 두 가지다**: `BrokerFailureMatrix` 의 클래스 javadoc 과, 저 테스트 메서드 이름(`everyStableAdapterCoversEveryFaultScenario` — 본문은 covers 를 검사하지 않는다). 이 리프의 나머지 javadoc 들이 자기 이력을 정확히 갱신해 온 것과 대비되어 눈에 띈다." }, { "line": 40363, "text": "" }, { "line": 40364, "text": "**(b) 선언된 project 의존 4개 중 2개가 import 0건.**" }, { "line": 40365, "text": "" }, { "line": 40366, "text": "```" }, { "line": 40367, "text": "messaging-core-api -> 사용 O" }, { "line": 40368, "text": "messaging-schema-api -> 사용 O (EncodedMessage)" }, { "line": 40369, "text": "messaging-policy -> import 0건" }, { "line": 40370, "text": "messaging-transport-spi -> import 0건" }, { "line": 40371, "text": "```" }, { "line": 40372, "text": "" }, { "line": 40373, "text": "`messaging-spring-cloud-stream-bridge`, `messaging-kafka-share-experimental` 에서 이미 본 것과 같은 형태다. 다만 여기는 §12.3(b) 때문에 성격이 다르다 — policy 를 **안 쓰는 게 아니라 써야 하는데 리터럴로 우회**하고 있다." }, { "line": 40374, "text": "" }, { "line": 40375, "text": "**(c) 증거의 커밋이 현재 트리가 아니다.**" }, { "line": 40376, "text": "" }, { "line": 40377, "text": "```" }, { "line": 40378, "text": "현재 HEAD : 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916" }, { "line": 40379, "text": "매니페스트의 gitCommit : e98b56eb03ecab588c21fd1e7dbcaa493c1d8645 (히스토리에 존재)" }, { "line": 40380, "text": "```" }, { "line": 40381, "text": "" }, { "line": 40382, "text": "이는 결함이 아니다. 게이트가 `gitCommit`/`observedAt` 을 비교에서 제외하는 것이 명시적 설계이며 그 이유가 주석에 있다. 다만 `BrokerCertificationEvidence` javadoc 이 \"the commit are here because 'certified' is a claim about … a specific source tree; without them the evidence cannot be checked against anything later\" 라고 쓴 것에 비해, 실제로 그 필드를 **읽어서 무언가를 판정하는 코드는 없다**. 기록은 되고 활용은 되지 않는다." }, { "line": 40383, "text": "" }, { "line": 40384, "text": "**(d) 지원 문서 9개 존재·내용 검사는 통과.** `MessagingDocumentationContractTest` 8건 전부 통과(`EVD-301`). 단, 이 검사는 `docs/messaging/support-matrix.md` 의 **등급 표기**만 본다. 같은 문서 23줄의 `runtime_memberships` 관련 서술 드리프트는 이 검사의 사정권 밖이며 §A19 에서 다룬다." }, { "line": 40385, "text": "" }, { "line": 40386, "text": "---" }, { "line": 40387, "text": "" } ], "numbered_context": "40142 | #### 10. 테스트 레인과 실제 증명 범위\n40143 | \n40144 | `EVD-301`: `./gradlew :messaging:messaging-testkit:test --rerun-tasks` → **44 tests, 0 failures, 0 errors, 0 skipped**.\n40145 | \n40146 | | 클래스 | 수 | 증명 대상 |\n40147 | |---|---:|---|\n40148 | | `CompatibilityMatrixTest` | 11 | 등급 규칙, 계약 크기 잠금, 미등록 어댑터 거절 |\n40149 | | `CrossBrokerContractSuite` | 10 | 증거→커버리지 변환, 기대 불일치 거절, 시나리오 불변식 |\n40150 | | `CertifiedEvidenceTest` | 8 | 매니페스트 원본성, 이미지/테스트ID 형식, gap 명명, 직렬화 왕복·거절 |\n40151 | | `MessagingDocumentationContractTest` | 8 | `docs/messaging/*.md` 9개 존재·내용·등급 일치 |\n40152 | | `InMemoryHarnessContractTest$Contract` | 7 | 공유 계약 7개 |\n40153 | \n40154 | **이 레인이 증명하는 것과 증명하지 않는 것의 경계가 이 리프의 핵심이다.**\n40155 | \n40156 | 증명한다: 매니페스트를 읽는 코드가 옳다. 등급이 매니페스트에서 파생된다. 문서가 등급과 일치한다. 계약이 7개다. 계약 7개가 결정론적 하니스에서 통과한다.\n40157 | \n40158 | 증명하지 않는다: **매니페스트에 든 4줄이 진짜 실행에서 나왔다는 것.** 그것은 `messaging-kafka:verifyMessagingCertificationEvidence` 만 증명하고, 그 레인은 `test` 에서 제외되어 있으며 Docker 를 요구한다. 이 세션에서 실행하지 않았다(§16).\n40159 | \n40160 | `MessagingDocumentationContractTest` 의 자기 제한이 좋다.\n40161 | \n40162 | ```java\n40163 | // MessagingDocumentationContractTest.java:18-20\n40164 | *
The assertions are deliberately narrow: they check the claims a reader would act on, not\n40165 | * prose. Asserting on wording would make every edit a test failure and the check would be deleted.\n40166 | ```\n40167 | \n40168 | 문서 검사가 삭제당하지 않도록 검사 범위를 스스로 좁혔다. 그리고 `noEnumConstantTheDocsDenyActuallyExists` 는 방향이 반대다 — 문서가 \"없다\"고 한 것(`EXACTLY_ONCE`, `GLOBAL`)이 실제로 enum 에 없는지를 확인한다. 문서의 **부정 주장**을 코드로 검증하는 것은 드문 패턴이다.\n40169 | \n40170 | ---\n40171 | \n40172 | #### 11. 빌드/ArchUnit/CI 강제 지점\n40173 | \n40174 | `jmh` 소스셋이 루트에서 정확히 3개 리프에만 부여된다.\n40175 | \n40176 | ```groovy\n40177 | // src/build.gradle:500-508\n40178 | // The three messaging leaves that carry JMH benchmarks get a `jmh` source set. It is a source\n40179 | // set rather than a plugin because the benchmarks are compiled and reviewed on every build but\n40180 | // only *run* on demand: a benchmark that stops compiling is a defect, while a benchmark that\n40181 | // runs in CI is a flaky test measuring the build agent.\n40182 | if (project.path in [':messaging:messaging-kafka',\n40183 | ':messaging:messaging-rabbit',\n40184 | ':messaging:messaging-testkit']) {\n40185 | ```\n40186 | \n40187 | 그 아래에서 `spotbugsJmh` 와 `checkstyleJmh` 를 **끈다**.\n40188 | \n40189 | ```groovy\n40190 | // src/build.gradle:528-535\n40191 | // JMH's annotation processor emits the generated harness into this source set, and its\n40192 | // generated code trips DLS_DEAD_LOCAL_STORE by design (the dead stores are how it defeats\n40193 | // dead-code elimination). … The benchmarks themselves are still compiled, which is what\n40194 | // catches a real breakage.\n40195 | tasks.named('spotbugsJmh') { enabled = false }\n40196 | tasks.named('checkstyleJmh') { enabled = false }\n40197 | ```\n40198 | \n40199 | 마지막 문장(\"still compiled\")이 참인지가 갈림길이다. 그 두 태스크가 `check → compileJmhJava` 로 가는 유일한 경로이기 때문이다. 실측했다(`EVD-298`):\n40200 | \n40201 | ```\n40202 | ./gradlew :messaging:messaging-testkit:build --dry-run\n40203 | :messaging:messaging-testkit:compileJmhJava SKIPPED\n40204 | :messaging:messaging-testkit:jmhClasses SKIPPED\n40205 | :messaging:messaging-testkit:checkstyleJmh SKIPPED\n40206 | :messaging:messaging-testkit:spotbugsJmh SKIPPED\n40207 | ```\n40208 | \n40209 | **참이다.** Gradle 의 `enabled = false` 는 태스크 액션만 건너뛰고 의존성 그래프는 유지하므로, 꺼진 `checkstyleJmh`/`spotbugsJmh` 가 여전히 `compileJmhJava` 를 끌고 들어온다. 벤치마크는 매 빌드에서 컴파일되고 실행만 온디맨드다. 반직관적이라 증거로 남겼다.\n40210 | \n40211 | `compileJmhJava` 에서 ErrorProne 을 끄고 `-Werror` 를 제거하는 이유도 명시적이다: \"ErrorProne's -Werror would reject JMH's generated sources, which the platform does not own and cannot fix.\"\n40212 | \n40213 | `jmh` 태스크는 `JavaExec` 로 `org.openjdk.jmh.Main` 을 부른다(`:536-541`).\n40214 | \n40215 | `EnvelopeCodecBenchmark` 가 무엇을 재는지에 대한 판단도 적혀 있다.\n40216 | \n40217 | ```java\n40218 | // EnvelopeCodecBenchmark.java:31-41\n40219 | /**\n40220 | * Measures the per-message cost the platform adds before any broker is involved.\n40221 | *\n40222 | *
This is the number the platform is accountable for. Broker latency dominates any real publish\n40223 | * and varies with the network, so measuring it would tell you about the test environment; …\n40224 | *\n40225 | *
Header validation is benchmarked separately from envelope construction because they scale\n40226 | * differently: construction is constant, while validation is linear in the header count …\n40227 | */\n40228 | ```\n40229 | \n40230 | 4개 벤치마크: `generateMessageId`(UuidV7), `validateFewHeaders`(3개), `validateManyHeaders`(32개), `buildEnvelope`. 헤더 맵을 `@Setup` 에서 미리 만들어 \"맵 생성이 아니라 검증을 잰다\"는 것을 보장한다.\n40231 | \n40232 | ---\n40233 | \n40234 | #### 12. 실제 사용 여부와 negative-space probes\n40235 | \n40236 | ##### 12.1 Public surface reachability\n40237 | \n40238 | **방법 주의.** 참조 계수는 단어 검색이 아니라 `import dev.caskeleton.messaging.testkit` 및 타입별 `git grep` 으로 셌다. 이 리프의 타입 이름(`CompatibilityMatrix`, `BrokerFailureMatrix` 등)은 저장소 내 동명 클래스가 없어 충돌은 없었으나, `isComplete`/`reset` 같은 **메서드 이름은 충돌이 심하다** — `git grep \"isComplete\"` 는 10건을 내지만 9건이 fileserver/websocket 의 무관한 클래스다(`EVD-299`, `EVD-300`). 메서드 단위 판정은 전부 소유 타입을 확인한 뒤 세었다.\n40239 | \n40240 | | 타입/멤버 | leaf 밖 참조 | 판정 |\n40241 | |---|---:|---|\n40242 | | `MessagingAdapterContract` | 2 (kafka, rabbit `extends`) | 사용됨 |\n40243 | | `MessagingAdapterHarness` | 2 (`implements`) | 사용됨 |\n40244 | | `ContractMessage` / `ContractAssertions` / `ObservedDelivery` / `HandleOutcome` | 2씩 | 사용됨 |\n40245 | | `DockerAvailability` | 4 모듈 | 사용됨 |\n40246 | | `NetworkFaultScenario`, `BrokerCertificationEvidence` | 1 (kafka 인증 IT) | 사용됨 |\n40247 | | `CertifiedEvidence` | 1 | 사용됨 |\n40248 | | `CompatibilityMatrix` | **0** (leaf 내부 테스트만) | leaf-local |\n40249 | | `BrokerFailureMatrix` | **0** (leaf 내부 테스트만) | leaf-local |\n40250 | | `FaultController.rejectPublish()` | **호출 0건** | §17 P2 |\n40251 | | `FaultController.reset()` | **호출 0건** | §17 P2 |\n40252 | | `BrokerFailureMatrix.adapters()` | **호출 0건** | §17 P3 |\n40253 | | `BrokerFailureMatrix.isComplete(...)` | 호출 1건, Experimental 에만 | §12.4 |\n40254 | \n40255 | `CompatibilityMatrix`/`BrokerFailureMatrix` 가 leaf 밖 참조 0인 것은 결함이 아니다. 이 둘의 소비자는 문서와 릴리스 판정이고, 그 판정은 이 리프의 테스트에서 이뤄지도록 설계되어 있다.\n40256 | \n40257 | `FaultController` 의 두 미사용 메서드는 다르다(`EVD-299`).\n40258 | \n40259 | ```\n40260 | dropPublishConfirmation() : 호출 1건 (MessagingAdapterContract:40)\n40261 | dropSettlementConfirmation(): 호출 1건 (MessagingAdapterContract:53)\n40262 | failDeadLetterPublish() : 호출 1건 (MessagingAdapterContract:104)\n40263 | rejectPublish() : 호출 0건\n40264 | reset() : 호출 0건\n40265 | ```\n40266 | \n40267 | 인터페이스 5개 중 3개만 계약이 쓴다. 나머지 2개는 **구현이 3벌 강제되면서 아무도 부르지 않는다**.\n40268 | \n40269 | ##### 12.2 Conditional sibling comparison\n40270 | \n40271 | 같은 저장소에 \"증거 기반 등급\" 을 하는 형제가 하나 더 있다.\n40272 | \n40273 | ```\n40274 | src/adapter/outbound/persistence-mongo/src/test/java/.../performance/MongoReleaseEvidenceTest.java\n40275 | MongoFailoverScenario.all().forEach(scenario -> gate.record(scenario, true));\n40276 | ```\n40277 | \n40278 | Mongo 쪽은 `gate.record(scenario, true)` 를 테스트가 직접 호출한다 — 즉 **테스트가 증거를 선언한다**. messaging 쪽은 매니페스트 파일이 증거를 나르고 테스트는 읽기만 한다. `CertifiedEvidence` 의 javadoc 이 고쳤다고 말하는 바로 그 형태가 Mongo 쪽에는 아직 남아 있다. 이는 이 리프의 결함이 아니라 **같은 교훈이 아직 전파되지 않은 곳**이며, family 문서에서 다룰 대비다.\n40279 | \n40280 | messaging 내부에서 `DockerAvailability` 를 쓰는 4개 모듈과 인증 레인의 관계도 대비된다: 전자는 없으면 skip, 후자는 가드 없이 실패 — §6 에 근거 인용.\n40281 | \n40282 | ##### 12.3 Duplicate mechanism sweep\n40283 | \n40284 | **(a) `Faults` 내부클래스 3중복 — 바이트 동일.** (`EVD-299`)\n40285 | \n40286 | ```\n40287 | KafkaContractHarness.java : 57줄 sha256[0:16]=3028b4591144fe86\n40288 | RabbitContractHarness.java : 57줄 sha256[0:16]=3028b4591144fe86\n40289 | InMemoryMessagingHarness.java: 57줄 sha256[0:16]=3028b4591144fe86\n40290 | diff kafka vs rabbit -> IDENTICAL\n40291 | diff kafka vs inmemory -> IDENTICAL\n40292 | ```\n40293 | \n40294 | `private static final class Faults implements FaultController` 57줄이 3개 모듈에 완전히 동일하게 존재한다. 총 171줄. 4개 불리언 필드 + 5개 오버라이드 + 4개 consume/query 메서드. 이 리프의 `src/main` 에 `DefaultFaultController` 하나만 두면 3벌이 1벌이 된다. 세 하니스가 `faults` 필드 타입만 공유하면 되므로 API 변경도 필요 없다.\n40295 | \n40296 | **(b) 1 MiB 한도 리터럴 8중복.** `PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576` 이 있는데도 같은 값이 리터럴로 다시 선언된다.\n40297 | \n40298 | ```\n40299 | messaging-policy/PayloadPolicy.java:17 DEFAULT_MAX_BYTES = 1_048_576 <- 정본\n40300 | messaging-schema-api/RawBytesMessageCodec.java:21 DEFAULT_MAX_BYTES = 1_048_576\n40301 | messaging-schema-json/JacksonMessageCodec.java:42 DEFAULT_MAX_BYTES = 1_048_576\n40302 | messaging-schema-avro/AvroMessageCodec.java:46 DEFAULT_MAX_BYTES = 1_048_576\n40303 | messaging-schema-protobuf/ProtobufMessageCodec.java:35 DEFAULT_MAX_BYTES = 1_048_576\n40304 | messaging-claim-check/…RetentionValidatorTest.java:47 PORTABLE_PAYLOAD_LIMIT_BYTES\n40305 | messaging-rabbit/RabbitContractHarness.java:40 MAX_PAYLOAD_BYTES\n40306 | messaging-testkit/InMemoryMessagingHarness.java:31 MAX_PAYLOAD_BYTES <- 이 리프\n40307 | messaging-testkit/ContractMessage.java:50 new byte[1_048_577] <- 이 리프\n40308 | messaging-spring-boot-starter/DestinationSettings.java:175 @DefaultValue(\"1048576\")\n40309 | ```\n40310 | \n40311 | `messaging-testkit` 은 `api project(':messaging:messaging-policy')` 를 선언하고 있으므로 `PayloadPolicy.DEFAULT_MAX_BYTES` 를 그냥 참조할 수 있다. §12.4 의 \"policy 미사용\" 과 합치면, 유일하게 policy 를 써야 할 자리에서 쓰지 않고 있는 셈이다.\n40312 | \n40313 | **(c) `hasLiveBrokerCertification` 대조 테스트의 항등식.**\n40314 | \n40315 | ```java\n40316 | // CompatibilityMatrixTest.java:48-60 aCertificationClaimCannotBeMadeWithoutEvidence\n40317 | assertThat(entry.hasLiveBrokerCertification())\n40318 | .isEqualTo(BrokerFailureMatrix.from(CertifiedEvidence.recorded()).hasLiveBrokerCoverage(entry.adapter()));\n40319 | ```\n40320 | \n40321 | `Entry.hasLiveBrokerCertification()` 의 본문이 정확히 우변과 같다(`CompatibilityMatrix.java:60-62`). 이 단언은 항상 참인 항등식이며, 어떤 회귀도 잡지 못한다. 같은 파일의 `everyStableAdapterIsCertifiedAgainstALiveBroker`(`:97-109`)와 `noExperimentalAdapterClaimsLiveBrokerCertification`(`:111-116`)이 실질 검사를 하고 있어 커버리지 손실은 없지만, 이름이 약속하는 것(\"증거 없이 인증 주장 불가\")을 이 테스트 자체는 검사하지 않는다.\n40322 | \n40323 | ##### 12.4 Documentation / measured-count drift\n40324 | \n40325 | **(a) `BrokerFailureMatrix` 클래스 javadoc 이 강제되지 않는 규칙을 선언한다.** (`EVD-300`)\n40326 | \n40327 | ```java\n40328 | // BrokerFailureMatrix.java:18-20\n40329 | *
A Stable adapter must cover every scenario. That rule is enforced by a test rather than\n40330 | * documented, because a promotion to Stable is exactly the moment the gap would otherwise be\n40331 | * overlooked.\n40332 | ```\n40333 | \n40334 | 측정:\n40335 | \n40336 | ```\n40337 | git grep -n \"isComplete\" -- src (messaging-testkit 범위)\n40338 | BrokerFailureMatrix.java:95 public boolean isComplete(String adapter) {\n40339 | CrossBrokerContractSuite.java:110 assertThat(matrix.isComplete(\"messaging-pulsar-experimental\"))\n40340 | ```\n40341 | \n40342 | `isComplete` 의 호출부는 1곳이고 그것은 **Experimental** 어댑터가 불완전함을 단언한다. Stable 어댑터에 `isComplete` 를 거는 테스트는 없다.\n40343 | \n40344 | 그리고 실제로 Stable 인 `messaging-kafka` 는 gap 을 가진 채 통과한다 — 그 사실이 같은 모듈에서 **명시적으로 단언되어 있다**.\n40345 | \n40346 | ```java\n40347 | // CertifiedEvidenceTest.java:52-55\n40348 | assertThat(CertifiedEvidence.knownGaps(\"messaging-kafka\"))\n40349 | .as(\"a Kafka producer buffers before it learns a connection exists, so this stays unproven\")\n40350 | .contains(NetworkFaultScenario.CONNECTION_REFUSED);\n40351 | ```\n40352 | \n40353 | 코드는 \"정직한 gap 열거\"로 바뀌었고 그 결정이 테스트 본문 주석에 남아 있다.\n40354 | \n40355 | ```java\n40356 | // CrossBrokerContractSuite.java:44-47\n40357 | void everyStableAdapterCoversEveryFaultScenario() {\n40358 | // The gaps are named rather than asserted empty. A Stable adapter with unrun scenarios is the\n40359 | // current, honest state; asserting emptiness here would only reinstate the self-declaration.\n40360 | ```\n40361 | \n40362 | **바뀌지 않은 것은 두 가지다**: `BrokerFailureMatrix` 의 클래스 javadoc 과, 저 테스트 메서드 이름(`everyStableAdapterCoversEveryFaultScenario` — 본문은 covers 를 검사하지 않는다). 이 리프의 나머지 javadoc 들이 자기 이력을 정확히 갱신해 온 것과 대비되어 눈에 띈다.\n40363 | \n40364 | **(b) 선언된 project 의존 4개 중 2개가 import 0건.**\n40365 | \n40366 | ```\n40367 | messaging-core-api -> 사용 O\n40368 | messaging-schema-api -> 사용 O (EncodedMessage)\n40369 | messaging-policy -> import 0건\n40370 | messaging-transport-spi -> import 0건\n40371 | ```\n40372 | \n40373 | `messaging-spring-cloud-stream-bridge`, `messaging-kafka-share-experimental` 에서 이미 본 것과 같은 형태다. 다만 여기는 §12.3(b) 때문에 성격이 다르다 — policy 를 **안 쓰는 게 아니라 써야 하는데 리터럴로 우회**하고 있다.\n40374 | \n40375 | **(c) 증거의 커밋이 현재 트리가 아니다.**\n40376 | \n40377 | ```\n40378 | 현재 HEAD : 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916\n40379 | 매니페스트의 gitCommit : e98b56eb03ecab588c21fd1e7dbcaa493c1d8645 (히스토리에 존재)\n40380 | ```\n40381 | \n40382 | 이는 결함이 아니다. 게이트가 `gitCommit`/`observedAt` 을 비교에서 제외하는 것이 명시적 설계이며 그 이유가 주석에 있다. 다만 `BrokerCertificationEvidence` javadoc 이 \"the commit are here because 'certified' is a claim about … a specific source tree; without them the evidence cannot be checked against anything later\" 라고 쓴 것에 비해, 실제로 그 필드를 **읽어서 무언가를 판정하는 코드는 없다**. 기록은 되고 활용은 되지 않는다.\n40383 | \n40384 | **(d) 지원 문서 9개 존재·내용 검사는 통과.** `MessagingDocumentationContractTest` 8건 전부 통과(`EVD-301`). 단, 이 검사는 `docs/messaging/support-matrix.md` 의 **등급 표기**만 본다. 같은 문서 23줄의 `runtime_memberships` 관련 서술 드리프트는 이 검사의 사정권 밖이며 §A19 에서 다룬다.\n40385 | \n40386 | ---\n40387 | ",
"headings": [
{
"line": 1,
"level": 1,
"text": "clean-architecture-backend-template — 상세 분석 (통합 정본)"
},
{
"line": 40,
"level": 2,
"text": "0. 이 문서를 읽는 법"
},
{
"line": 60,
"level": 2,
"text": "1. Project map — 숫자로 먼저"
},
{
"line": 62,
"level": 3,
"text": "1.1 빌드와 레지스트리"
},
{
"line": 81,
"level": 3,
"text": "1.2 가족별 분모와 출하 여부"
},
{
"line": 94,
"level": 3,
"text": "1.3 leaf별 규모 (main Java 기준 상위)"
},
{
"line": 119,
"level": 3,
"text": "1.4 이 표에서 읽어야 할 것"
},
{
"line": 168,
"level": 2,
"text": "2. Architectural boundaries — 무엇이 경계를 강제하는가"
},
{
"line": 173,
"level": 3,
"text": "2.1 강제 장치 목록"
},
{
"line": 189,
"level": 3,
"text": "2.2 `CleanArchitectureTest`의 규칙 14종"
},
{
"line": 212,
"level": 3,
"text": "2.3 검증된 경계 — 실제로 성립하는 것"
},
{
"line": 266,
"level": 3,
"text": "2.4 경계가 열려 있는 지점"
},
{
"line": 300,
"level": 2,
"text": "3. Representative execution paths"
},
{
"line": 302,
"level": 3,
"text": "3.1 HTTP 요청 — 출하 경로"
},
{
"line": 364,
"level": 3,
"text": "3.2 트랜잭션 — `application-core` 포트에서 PostgreSQL local timeout까지"
},
{
"line": 453,
"level": 3,
"text": "3.3 메시지 발행 — messaging 플랫폼"
},
{
"line": 494,
"level": 3,
"text": "3.4 gRPC — 채택 시점 경로"
},
{
"line": 518,
"level": 3,
"text": "3.5 알림 발송 — 논리적 수락과 provider 불확실성"
},
{
"line": 539,
"level": 2,
"text": "4. Data and state"
},
{
"line": 541,
"level": 3,
"text": "4.1 관계형 — `persistence-jpa` (605 파일 / main 350 / 27,744 LOC)"
},
{
"line": 654,
"level": 3,
"text": "4.2 문서형 — `persistence-mongo` (497 파일 / main 351 / 22,924 LOC)"
},
{
"line": 705,
"level": 3,
"text": "4.3 messaging 신뢰성 저장소 (`19` §7)"
},
{
"line": 757,
"level": 3,
"text": "4.4 fileserver / objectstorage / cache-redis"
},
{
"line": 788,
"level": 2,
"text": "5. Failure and operational behavior"
},
{
"line": 790,
"level": 3,
"text": "5.1 실패 분류 — 세 개의 계층"
},
{
"line": 824,
"level": 3,
"text": "5.2 관측 — 태그를 유한하게, 그리고 그 대가"
},
{
"line": 854,
"level": 3,
"text": "5.3 시작 검증기 — 법칙과 그 예외"
},
{
"line": 903,
"level": 3,
"text": "5.4 admin plane — 가장 잘 조립된 게이트"
},
{
"line": 939,
"level": 3,
"text": "5.5 gRPC 구현 층의 원자성 (`20` §7)"
},
{
"line": 1011,
"level": 2,
"text": "6. Tests and verification coverage"
},
{
"line": 1013,
"level": 3,
"text": "6.1 실행한 것"
},
{
"line": 1025,
"level": 3,
"text": "6.2 실행하지 않은 것과 그 이유"
},
{
"line": 1047,
"level": 3,
"text": "6.3 fail-closed 레인 규약"
},
{
"line": 1071,
"level": 3,
"text": "6.4 완전히 닫힌 게이트 하나 — messaging 인증 체인"
},
{
"line": 1111,
"level": 3,
"text": "6.5 evidence manifest — JPA의 R1/R2 분리"
},
{
"line": 1125,
"level": 3,
"text": "6.6 게이트가 통과하면서 아무것도 증명하지 않는 경우 — 14건"
},
{
"line": 1156,
"level": 2,
"text": "7. 이 저장소에서 반복된 네 가지 형태"
},
{
"line": 1160,
"level": 3,
"text": "7.1 형태 A — 판정하는 코드는 있고, 부르는 코드가 없다"
},
{
"line": 1203,
"level": 3,
"text": "7.2 형태 B — 게이트가 통과하면서 아무것도 증명하지 않는다"
},
{
"line": 1214,
"level": 3,
"text": "7.3 형태 C — 중복 장치에서 조립된 쪽이 약한 쪽이다"
},
{
"line": 1239,
"level": 3,
"text": "7.4 형태 D — 문서 드리프트, 그리고 그 방향"
},
{
"line": 1274,
"level": 3,
"text": "7.5 공시 스펙트럼 — 자기 미완성을 얼마나 말했는가"
},
{
"line": 1289,
"level": 3,
"text": "7.6 학습 전이 — messaging → grpc"
},
{
"line": 1308,
"level": 2,
"text": "8. Confirmed problems"
},
{
"line": 1310,
"level": 3,
"text": "8.1 P1 — 지금 출하되는 아티팩트에서 틀린 동작"
},
{
"line": 1349,
"level": 3,
"text": "8.2 P2 — 명확한 실패 시나리오를 가진 실질적 공백"
},
{
"line": 1392,
"level": 3,
"text": "8.3 심각도가 등급 때문에 낮아진 것"
},
{
"line": 1403,
"level": 2,
"text": "9. Reusable criteria and rules"
},
{
"line": 1452,
"level": 2,
"text": "10. Explicit project decisions"
},
{
"line": 1457,
"level": 3,
"text": "10.1 계약과 경계"
},
{
"line": 1468,
"level": 3,
"text": "10.2 실패와 불확실성"
},
{
"line": 1480,
"level": 3,
"text": "10.3 조립과 활성화"
},
{
"line": 1492,
"level": 3,
"text": "10.4 데이터와 경계값"
},
{
"line": 1506,
"level": 3,
"text": "10.5 증거와 게이트"
},
{
"line": 1523,
"level": 2,
"text": "11. Unresolved questions"
},
{
"line": 1564,
"level": 2,
"text": "12. Evidence index"
},
{
"line": 1581,
"level": 2,
"text": "13. Limits of this analysis"
},
{
"line": 1632,
"level": 2,
"text": "14. 사이클 2 — 18개 리프 재검증과 23개 리프 전수 통독"
},
{
"line": 1634,
"level": 3,
"text": "14.1 18개 리프 재검증"
},
{
"line": 1668,
"level": 3,
"text": "14.2 23개 리프 전수 통독"
},
{
"line": 1747,
"level": 2,
"text": "부록 A. 모듈 문서 지도"
},
{
"line": 1779,
"level": 2,
"text": "부록 B. 자주 쓸 명령"
},
{
"line": 1825,
"level": 2,
"text": "부록 C. 다시 읽는다면 이 순서"
},
{
"line": 1839,
"level": 1,
"text": "제2부 — 모듈 분석 전문"
},
{
"line": 1845,
"level": 2,
"text": "A00. project-overview"
},
{
"line": 1849,
"level": 3,
"text": "Project Overview"
},
{
"line": 1856,
"level": 4,
"text": "분석 기준 revision"
},
{
"line": 1867,
"level": 4,
"text": "최종 커버리지"
},
{
"line": 1884,
"level": 4,
"text": "Build and module map"
},
{
"line": 1939,
"level": 4,
"text": "Dependency direction"
},
{
"line": 1945,
"level": 4,
"text": "Runtime entry points"
},
{
"line": 1951,
"level": 4,
"text": "Persistence / messaging / external systems"
},
{
"line": 1955,
"level": 4,
"text": "Test topology"
},
{
"line": 1960,
"level": 4,
"text": "Configuration and operational surfaces"
},
{
"line": 1964,
"level": 4,
"text": "분석할 bounded scopes (계획 — 실제 문서 배치는 위 \"최종 커버리지\" 참조)"
},
{
"line": 1977,
"level": 4,
"text": "아직 단정하지 않는 것 (분석 시작 시점의 목록)"
},
{
"line": 1993,
"level": 2,
"text": "A01. domain-core"
},
{
"line": 1997,
"level": 3,
"text": "domain-core 상세 분석"
},
{
"line": 2000,
"level": 4,
"text": "SSOT identity — 2026-08-31 재검증"
},
{
"line": 2015,
"level": 4,
"text": "분석 범위와 결론 상태"
},
{
"line": 2026,
"level": 4,
"text": "1. Quantified scope map"
},
{
"line": 2028,
"level": 5,
"text": "Owned source"
},
{
"line": 2042,
"level": 4,
"text": "2. Coverage ledger"
},
{
"line": 2062,
"level": 4,
"text": "3. 이 모듈이 실제로 소유하는 것"
},
{
"line": 2064,
"level": 5,
"text": "관찰: 재사용 가능한 도메인 “내용”보다 도메인 모델링 계약을 소유한다"
},
{
"line": 2073,
"level": 4,
"text": "4. Identifier contract"
},
{
"line": 2075,
"level": 5,
"text": "`ResourceId