# messaging-kafka-share-experimental 완전 해부 > 상태: COMPLETE > 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916` > 분석 범위: `src/messaging/messaging-kafka-share-experimental` > SSOT owner: `messaging-kafka-share-experimental` > integration/family document: `analysis/19-messaging-platform.md` (secondary, INTEGRATION_ONLY) --- ## 0. SSOT identity / 커버리지와 숫자 지도 - registered leaf id: `messaging-kafka-share-experimental` - canonical state `analysisFile`: `analysis/messaging/messaging-kafka-share-experimental.md` - source path: `src/messaging/messaging-kafka-share-experimental` - registry `allowed_dependencies`: `["messaging-core-api", "messaging-policy", "messaging-transport-spi", "messaging-kafka"]` - registry `runtime_memberships`: **`[]`** — build-only / incubating ### 숫자 | 항목 | 수 | |---|---:| | production Java 파일 | **4** | | production LOC | **190** — messaging family에서 가장 작다 | | 패키지 | 1 (`dev.caskeleton.messaging.kafka.share`) | | test 파일 | 1 | | test 메서드(실행 확인) | 6 | | 선언된 외부 의존성 | 1 (`org.apache.kafka:kafka-clients`, `implementation`) | | **실제 사용된 외부 의존성** | **0**(§12.4) | 네 타입: | 타입 | 종류 | LOC | leaf 밖 참조 | |---|---|---:|---:| | `KafkaShareGroupRegistrar` | class | 88 | **0** | | `KafkaShareProfileValidator` | class | 42 | **0** | | `KafkaShareProfile` | record | 33 | **0** | | `KafkaShareWorkQueueCapability` | class | 27 | **0** | ### Coverage ledger | scope/file group | count | disposition | reason | |---|---:|---|---| | `src/main/java/**` (4) | 4 | `FULL_READ` | 전 파일 본문 확인 | | `src/test/java/**` (1) | 1 | `FULL_READ` | 6개 테스트 확인 | | `build.gradle` | 1 | `FULL_READ` | 10줄 | | `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일 | | `build/**` | — | `EXCLUDED` | 빌드 산출물 | `UNCLASSIFIED` 0. --- ## 1. 모듈의 정체와 경계 Kafka **Share Group**(KIP-932, 경쟁 소비자 work queue)을 실험적 어댑터로 감싼다. `runtime_memberships: []`이고 이름 자체가 `-experimental`이다. 이 leaf의 실질은 **거절**이다. 190줄 중 실제 동작을 하는 코드는 거의 없고, 세 가지를 거절한다. | 거절 | 코드 | 이유 | |---|---|---| | 비활성 상태의 사용 | `KAFKA_SHARE_DISABLED` | experimental이 기본 켜지지 않게 | | 순서 보장 목적지 | `IllegalArgumentException` | share group이 순서를 줄 수 없음 | | pause/resume | `KAFKA_SHARE_NO_PAUSE`/`_NO_RESUME` | 일시정지할 파티션 할당이 없음 | 핵심 진술이 validator javadoc에 있다. ```java // KafkaShareProfileValidator.java:10-17 *

A share group hands individual records to competing consumers and acknowledges them * individually. That is a work queue, and it is fundamentally incompatible with partition ordering: * two consumers in the same share group can process records from one partition concurrently and * finish in either order. Configuring an ordered destination on a share group would therefore * advertise a guarantee the broker is not providing, so it is refused rather than degraded. * *

The adapter is also off unless explicitly enabled, so an Experimental capability cannot drift * into a Stable deployment by default. ``` 두 번째 문단이 이 저장소의 experimental 정책을 한 문장으로 담는다 — **기본 꺼짐이 drift 방지 수단이다.** --- ## 2. 의존성과 런타임 배선 들어오는 것(project): `messaging-core-api`, `messaging-policy`, `messaging-transport-spi`, `messaging-kafka` — 넷 다 `api`. 들어오는 것(vendor): `org.apache.kafka:kafka-clients`(`implementation`) — **어떤 소스도 import하지 않는다**(§12.4). 나가는 것: **없다.** 어떤 leaf의 `allowed_dependencies`에도 이 leaf가 없고 starter 목록에도 없다. 런타임 배선: 없음. `runtime_memberships: []`. bean 없음(Spring 주석 0개). **소비자 없음·membership 없음·조립 없음의 삼중 정합**이다 — `messaging-schema-avro`·`messaging-schema-protobuf`와 같은 형태이고, incubating leaf의 올바른 상태다. **`messaging-policy`와 `messaging-kafka` 의존이 실제로 쓰이는가.** | 의존 | 사용 | |---|---| | `messaging-core-api` | `OrderingScope`, `DestinationName`, `MessagingCapabilities`, `MessagingCapabilityUnavailableException` — **사용** | | `messaging-transport-spi` | `TransportConsumerRegistration`, `TransportConsumerSpec` — **사용** | | `messaging-policy` | 어떤 타입도 import하지 않음 — **미사용** | | `messaging-kafka` | 어떤 타입도 import하지 않음 — **미사용** | 네 project 의존 중 둘, 벤더 의존 하나가 미사용이다. §17. --- ## 3. 패키지/컴포넌트 지도 ``` KafkaShareProfile (record) destination · shareGroup · orderingScope · enabled · maxDeliveryCount ↓ KafkaShareProfileValidator.validate(profile) ├── !enabled → MessagingCapabilityUnavailableException(KAFKA_SHARE_DISABLED) └── orderingScope != NONE → IllegalArgumentException ↓ KafkaShareGroupRegistrar.register(profile, spec) └── ShareRegistration implements TransportConsumerRegistration ├── pause(scope) → failedFuture(KAFKA_SHARE_NO_PAUSE) ├── resume(scope) → failedFuture(KAFKA_SHARE_NO_RESUME) ├── isActive() → true until close() └── close() → active = false KafkaShareWorkQueueCapability.capabilities() → MessagingCapabilities(12 booleans) ``` --- ## 4. 계약·불변식·상태 모델 ### 4.1 `KafkaShareProfile` 다섯 필드. 생성자가 `shareGroup` 공백과 `maxDeliveryCount < 1`을 거절한다. `maxDeliveryCount`가 javadoc에서 "how many times a record may be re-acquired before it is released"라고 정의된다 — Share Group의 재획득 한계다. **이 필드를 읽는 코드가 이 leaf에 없다.** validator도 registrar도 쓰지 않는다. ### 4.2 `KafkaShareProfileValidator` — 두 거절 ```java if (!profile.enabled()) { throw new MessagingCapabilityUnavailableException( "KAFKA_SHARE_DISABLED", "the Kafka Share Group adapter is experimental and disabled unless " + "backend.messaging.experimental.kafka-share=true"); } if (profile.orderingScope() != OrderingScope.NONE) { throw new IllegalArgumentException( "a Kafka share group cannot provide ordered delivery: " + profile.destination().value()); } ``` **두 거절의 예외 타입이 다르다.** 첫째는 `MessagingCapabilityUnavailableException`(카테고리 `CONFIGURATION`, 안정 코드 있음), 둘째는 `IllegalArgumentException`(코드 없음). 둘 다 설정 오류인데 하나만 플랫폼 실패 어휘를 쓴다. §17. 에러 메시지가 **프로퍼티 키를 직접 적는다** — `backend.messaging.experimental.kafka-share=true`. 그 키를 읽는 코드가 이 저장소에 없다(§12.4). ### 4.3 `KafkaShareGroupRegistrar` — spec을 받고 쓰지 않는다 ```java public TransportConsumerRegistration register( KafkaShareProfile profile, TransportConsumerSpec spec) { Objects.requireNonNull(spec, "spec must not be null"); validator.validate(profile); return new ShareRegistration(profile); } ``` `spec`은 **null 검사만 받는다.** `ShareRegistration`은 `profile`과 `AtomicBoolean active` 둘만 갖는다. `TransportConsumerSpec`은 `(DestinationProfile profile, Function> sink)`이고, `sink`가 플랫폼이 전달마다 부르는 콜백이다(`messaging-transport-spi` §4.5). 그 sink가 저장되지 않으므로 **어떤 메시지도 전달되지 않는다.** Kafka 소비자도 만들어지지 않는다 — `kafka-clients`를 import하는 코드가 없다. 즉 `register(...)`는 **아무것도 등록하지 않고** `isActive() == true`인 객체를 반환한다. §17. ### 4.4 `ShareRegistration` — pause/resume은 실패 stage ```java @Override public CompletionStage pause(String scope) { return CompletableFuture.failedFuture( new MessagingCapabilityUnavailableException("KAFKA_SHARE_NO_PAUSE", ...)); } ``` registrar javadoc이 이유를 적는다. ```java // :12-15 *

Pause and resume are refused rather than silently ignored. A share group has no partition * assignment to pause, so accepting the call would let a retry policy that depends on pausing * appear to work while doing nothing. ``` **예외를 던지지 않고 실패한 `CompletionStage`를 반환한다** — `TransportConsumerRegistration.pause`의 반환 타입이 `CompletionStage`이므로 비동기 계약을 지킨다. `messaging-runtime-core`의 `DefaultDeliveryProcessor.OneShotSettlement`가 이중 정산을 `failedFuture`로 보고하는 것과 같은 규율이다. 이 거절이 `messaging-policy`의 `RetryMode.PAUSE_PARTITION`과 맞물린다 — 그 모드를 share group 목적지에 설정하면 `DefaultRetryDecisionEngine`이 `PauseAndRetry`를 고르고 이 registration이 그것을 거절한다. **두 leaf가 같은 사실을 양쪽에서 안다.** `close()`가 `active`를 false로 바꾸는 것 외에 아무것도 하지 않는다 — 해제할 자원이 없기 때문이다. ### 4.5 `KafkaShareWorkQueueCapability` — 12개 boolean ```java return new MessagingCapabilities( true, true, true, false, false, false, false, false, false, false, false, false); ``` `MessagingCapabilities`의 필드 순서에 대입하면: | # | capability | 값 | |---:|---|:---:| | 1 | `brokerAcknowledgement` | **true** | | 2 | `replicationOrPersistenceEvidence` | **true** | | 3 | `perMessageSettlement` | **true** | | 4 | `batchSettlement` | false | | 5 | `orderedStream` | false | | 6 | `keyedOrdering` | false | | 7 | `replay` | false | | 8 | `delayedDelivery` | false | | 9 | `brokerTransaction` | false | | 10 | `deduplicatedPublish` | false | | 11 | `nativeDeadLetter` | false | | 12 | `topologyManagement` | false | javadoc이 요약한다 — "Per-record settlement, yes. Ordering, replay, and transactions, no — a share group gives up exactly those to gain competing-consumer throughput." **세 true가 정확히 3·1·2번**이고 javadoc이 "per-record settlement"만 언급한다. 1·2번(브로커 ack, 복제 증거)은 언급되지 않는다. 선언 목적도 적혀 있다 — "Declared as a capability rather than assumed, so that the shared validators refuse an ordered or replayed destination on this adapter before a message is ever produced." 즉 `messaging-policy`의 검증기와 `DefaultRetryDecisionEngine`이 이 값을 읽을 것을 전제한다. **그 전달 경로가 없다**(§12.1). --- ## 5. 주요 실행 경로 **등록:** `registrar.register(profile, spec)` → `validator.validate(profile)` → 통과하면 `ShareRegistration(profile)` 반환 → **이후 아무 일도 일어나지 않는다** **pause:** `registration.pause(scope)` → 즉시 실패 stage 이 leaf에 메시지가 흐르는 경로가 없다. --- ## 6. 실패 경로와 복구/번역 | 코드 | 예외 | 카테고리 | 조건 | |---|---|---|---| | `KAFKA_SHARE_DISABLED` | `MessagingCapabilityUnavailableException` | `CONFIGURATION` | `enabled == false` | | (코드 없음) | `IllegalArgumentException` | — | `orderingScope != NONE` | | `KAFKA_SHARE_NO_PAUSE` | `MessagingCapabilityUnavailableException` | `CONFIGURATION` | `pause(...)` | | `KAFKA_SHARE_NO_RESUME` | `MessagingCapabilityUnavailableException` | `CONFIGURATION` | `resume(...)` | | (코드 없음) | `IllegalArgumentException` | — | `shareGroup` 공백, `maxDeliveryCount < 1` | `MessagingCapabilityUnavailableException`의 javadoc이 이 leaf의 태도와 정확히 일치한다 — "Thrown instead of quietly degrading. Downgrading … ordered delivery to unordered, produces a system that looks healthy right up to the moment the guarantee actually mattered." --- ## 7. 트랜잭션·동시성·수명주기 트랜잭션 없음. `ShareRegistration.active`가 `AtomicBoolean`이다. `close()`가 `set(false)`이고 CAS가 아니므로 두 번 닫아도 무해하다(멱등). `KafkaShareProfileValidator`·`KafkaShareWorkQueueCapability`는 상태가 없다. `KafkaShareGroupRegistrar`는 validator 참조 하나만 갖는다. 수명주기 참여 없음 — `TransportConsumerRegistration`이 `AutoCloseable`이지만 이 구현은 닫을 자원을 갖지 않는다. --- ## 8. 설정·기능 플래그·환경 차이 | 항목 | 값 | |---|---| | 프로퍼티 키(에러 메시지에만 등장) | `backend.messaging.experimental.kafka-share` | | `enabled` | `KafkaShareProfile`의 필드 — 호출자가 채운다 | **그 프로퍼티를 읽는 코드가 저장소에 없다.** `enabled`는 `KafkaShareProfile` 생성자 인자이고 그 profile을 만드는 production 코드도 없다. 즉 키는 문서로만 존재한다. §17. 상수 없음. --- ## 9. 퍼시스턴스/외부 시스템 세부 **없다.** Kafka Share Group을 감싼다고 선언하지만 Kafka 클라이언트를 사용하지 않는다. `build.gradle`이 `implementation 'org.apache.kafka:kafka-clients'`를 선언하고 `import org.apache.kafka`가 소스에 0건이다(§12.4). --- ## 10. 테스트 레인과 실제 증명 범위 레인: `./gradlew :messaging:messaging-kafka-share-experimental:test`. **BUILD SUCCESSFUL, 6 tests, 0 skipped, 0 failures**. | 클래스 | 수 | 실제로 증명하는 것 | 증명하지 않는 것 | |---|---:|---|---| | `KafkaShareProfileValidatorTest` | 6 | 두 거절 조건과 통과 조건 | **registrar·capability가 검증되지 않음** | **네 타입 중 하나만 테스트된다.** - `KafkaShareGroupRegistrar` — 테스트 없음. `register`가 spec을 무시하는 것, pause/resume이 실패 stage를 반환하는 것, `close`가 `isActive`를 바꾸는 것이 전부 미검증 - `KafkaShareWorkQueueCapability` — 테스트 없음. 12개 boolean 중 어느 것도 단언되지 않음 - `KafkaShareProfile` — 생성자 거절 둘이 validator 테스트를 통해 간접적으로만 `messaging-schema-avro`가 3개 테스트 클래스로 2개 production 타입을 덮는 것과 대비된다. --- ## 11. 빌드/ArchUnit/CI 강제 지점 | 게이트 | 이 leaf에 대해 | |---|---| | `verifyCleanArchitectureDependencies` | 네 project 의존 — **미사용 둘을 포함해 통과한다**(허용 목록은 상한이지 하한이 아니다) | | `verifyRuntimeModuleMembership` | `[]` — 런타임 편입 없음이 강제됨 | | vendor `api` 규칙(`src/messaging/CLAUDE.md:40-43`) | public 시그니처에 Kafka 타입이 없으므로 `implementation`이 맞다. **다만 아예 쓰이지 않는다**(§12.4) | | `SecretLeakStaticScanTest`(observability leaf) | 이 leaf 소스도 스캔 대상 | | ArchUnit | 전용 규칙 없음 | 첫 행이 이 leaf의 §17 항목 중 하나다 — `allowed_dependencies`가 **실제 사용을 요구하지 않는다.** --- ## 12. 실제 사용 여부와 negative-space probes 원시 증거: `evidence/raw/290-claimcheck-and-kafkashare-unconsumed.txt` §C·§D·§E. ### 12.1 Public surface reachability **네 타입 전부 leaf 밖 참조 0이다.** ``` KafkaShareGroupRegistrar 0 KafkaShareProfile 0 KafkaShareProfileValidator 0 KafkaShareWorkQueueCapability 0 ``` `runtime_memberships: []`, starter 미포함, 조립 0건 — **삼중 정합**이다. `messaging-schema-avro`·`messaging-schema-protobuf`와 같은 상태이고, incubating leaf가 이래야 하는 형태다. `messaging-claim-check`·`messaging-cloudevents`와 대비된다 — 그 둘은 소비자 0인데 membership이 있다. **`KafkaShareWorkQueueCapability`의 0이 다른 의미를 갖는다.** 이 클래스의 javadoc은 "so that the shared validators refuse an ordered or replayed destination on this adapter before a message is ever produced"라고 한다. 즉 **공유 검증기가 이 값을 읽을 것을 전제한다.** `messaging-policy`의 `RetryContext.capabilities`와 `DefaultRetryDecisionEngine`이 그 소비자인데, 그것에 이 값을 넘기는 경로가 없다. `MessagingTransport.capabilities(DestinationName)`가 그 경로여야 하는데 이 leaf는 `MessagingTransport`를 구현하지 않는다. ### 12.2 Conditional sibling comparison Spring 주석 0개, bean 없음. **`MessagingTransport` 구현 sibling과의 비교가 유의미하다.** | 어댑터 leaf | `MessagingTransport` 구현 | membership | |---|:---:|---| | `messaging-kafka` | o (`KafkaMessagingTransport`) | `["app-bootstrap"]` | | `messaging-rabbit` | o (`RabbitMessagingTransport`) | `["app-bootstrap"]` | | `messaging-pulsar-experimental` | o (`PulsarMessagingTransport`) | `[]` | | `messaging-nats-experimental` | o (`NatsJetStreamTransport`) | `[]` | | **`messaging-kafka-share-experimental`** | **x** | `[]` | **네 형제 어댑터가 전부 SPI를 구현하고 이 leaf만 구현하지 않는다.** 두 experimental 형제(pulsar, nats)도 구현한다. 그래서 이 leaf는 "experimental이라서 미완"이 아니라 **형제와 다른 형태**다 — `TransportConsumerRegistration`만 부분 구현하고 `MessagingTransport`는 건드리지 않는다. 결과: capability 선언(§12.1)도, 발행 경로도, 소비 경로도 플랫폼에 연결될 지점이 없다. ### 12.3 Duplicate mechanism sweep **(a) 순서 거절이 두 곳에 있다** | 위치 | 검사 | |---|---| | 이 leaf `KafkaShareProfileValidator` | `orderingScope != NONE` → 거절 | | `messaging-policy` `DestinationProfileValidator:78-82` | `orderingScope == DESTINATION && consumer.concurrency > 1` → 거절 | | `messaging-policy` `DestinationProfileValidator:83-87` | `isOrdered() && maxInFlightPerOrderingUnit > 1` → 거절 | 세 검사가 같은 관심사(순서와 동시성의 양립 불가)를 다룬다. 이 leaf의 것이 가장 강하다 — **순서 자체를 금지**한다. policy 쪽은 순서를 허용하되 동시성을 1로 묶는다. 두 정책이 만나는 지점이 없다 — 이 leaf가 `DestinationProfile`을 받지 않고 자기 `KafkaShareProfile`을 쓴다. 즉 **목적지 프로파일 하나가 두 검증기를 통과하는 경로가 없다.** 중복이 아니라 **연결되지 않은 두 모델**이다. **(b) `enabled` 플래그 패턴** experimental leaf 셋(`kafka-share`, `pulsar`, `nats`) 중 이 leaf만 `enabled`를 profile 필드로 갖는다. 나머지 둘의 활성화 방식은 각 leaf SSOT가 답한다. **(c) `MessagingCapabilities` 선언이 어댑터마다** 각 어댑터가 자기 capability 집합을 선언한다. 이 leaf는 정적 메서드 하나, `messaging-kafka`는 `KafkaMessagingTransport.CAPABILITIES` 상수. 형태가 다르지만 중복 경쟁은 아니다 — 각자 자기 브로커를 서술한다. ### 12.4 Documentation / measured-count drift | 문서 주장 | 재측정 | 결과 | |---|---|---| | `build.gradle`: `kafka-clients` 의존 | `import org.apache.kafka` **0건** | **미사용 의존** | | registry: `messaging-policy`·`messaging-kafka` 의존 | 두 패키지에서 import 0건 | **미사용 의존** | | `KafkaShareProfileValidator` 에러 메시지: `backend.messaging.experimental.kafka-share=true` | 그 키를 읽는 코드 0건 | **미실현** | | `KafkaShareWorkQueueCapability` javadoc: "the shared validators refuse … before a message is ever produced" | capability를 검증기로 넘기는 경로 없음 | **미실현** | | `KafkaShareGroupRegistrar` javadoc: "Registers a share group consumer" | 소비자를 만들지 않음 | **불일치** | | `support-matrix.md:23`: 모든 messaging leaf가 unwired | 이 leaf는 실제로 `[]` | **이 leaf에 한해 참** | 다섯 번째가 이 leaf의 가장 무거운 drift다 — 클래스 이름과 메서드 이름이 하지 않는 일을 서술한다. --- ## 13. Git/설계 문서에서 확인한 변화와 실패 기록 이 leaf의 javadoc에 **이전 결함 서술이 없다.** 다른 messaging leaf 대부분이 "X used to …" 형태의 기록을 갖는 것과 대비된다. 대신 **막으려는 것**을 셋 적는다. | 위치 | 막으려는 것 | |---|---| | `KafkaShareProfileValidator` | 순서 목적지를 share group에 설정 → 브로커가 주지 않는 보장을 광고 | | 같은 곳 | experimental이 기본 켜져 Stable 배포로 drift | | `KafkaShareGroupRegistrar` | pause를 조용히 무시 → pause에 의존하는 retry 정책이 동작하는 것처럼 보이며 아무것도 하지 않음 | 세 번째가 이 leaf에서 가장 성숙한 판단이다 — **거절이 무시보다 낫다**는 원칙이고, `messaging-core-api`의 `MessagingCapabilityUnavailableException` javadoc과 같은 계열이다. 역설적으로 **그 원칙이 `register(...)`에는 적용되지 않았다** — spec을 받아 무시하고 성공을 반환한다(§17). --- ## 14. 런타임·터미널 Evidence | id | 종류 | 파일 | 무엇을 보여주는가 | 한계 | |---|---|---|---|---| | EVD-290 | command | `evidence/raw/290-claimcheck-and-kafkashare-unconsumed.txt` §C·§D·§E | 네 타입 참조 0, `kafka-clients` 선언과 import 0(exit=1), `register`가 spec을 무시하는 코드와 `ShareRegistration` 필드 | 정적 검색 | | EVD-293 | command | `./gradlew :messaging:messaging-kafka-share-experimental:test --rerun-tasks` | BUILD SUCCESSFUL, 6 / 0 / 0 | validator만 검증 | --- ## 15. 명시적 설계 이유와 추론을 구분한 정리 **명시적** - share group이 순서와 양립 불가인 이유 — `KafkaShareProfileValidator` javadoc - experimental이 기본 꺼짐인 이유 — 같은 javadoc - pause/resume을 무시하지 않고 거절하는 이유 — `KafkaShareGroupRegistrar` javadoc - capability를 선언으로 두는 이유 — `KafkaShareWorkQueueCapability` javadoc - share group이 포기한 것(순서·replay·트랜잭션)과 얻은 것(경쟁 소비자 처리량) — 같은 javadoc **추론** - `register`가 spec을 쓰지 않는 것이 미완인지 의도인지 → **미상**. 다른 형제 어댑터는 전부 실제 소비자를 만든다. - `kafka-clients`·`messaging-policy`·`messaging-kafka` 의존이 선언만 된 이유 → **추론**. 완성된 구현을 상정하고 미리 선언한 것으로 보인다. - `maxDeliveryCount`를 읽는 코드가 없는 이유 → **미상**. 같은 추론이 적용된다. --- ## 16. 확인한 것 / 확인하지 못한 것 **확인한 것** - 4개 타입 190줄 전문 - 6개 테스트가 통과하고 validator만 덮는다는 것 - 네 타입 전부 참조 0이고 membership `[]`과 정합한다는 것 - `kafka-clients` 의존 선언과 import 0건 - `messaging-policy`·`messaging-kafka` 의존이 사용되지 않는다는 것 - `register(...)`가 `TransportConsumerSpec`을 null 검사만 하고 버린다는 것 - 형제 어댑터 넷이 전부 `MessagingTransport`를 구현하고 이 leaf만 하지 않는다는 것 **확인하지 못한 것** - 이 leaf를 완성할 계획이 있는지 — 커밋이 대량 커밋뿐이고 기록이 없다. - Kafka Share Group(KIP-932)이 이 저장소가 고정한 Kafka 버전에서 사용 가능한지 — `kafka-clients` 버전이 lockfile에 있으나 확인하지 않았다. - `backend.messaging.experimental.kafka-share` 키가 어딘가 문서화돼 있는지 — `docs/messaging/experimental-policy.md`가 후보다. - `maxDeliveryCount`가 어떤 값을 갖도록 의도됐는지. --- ## 17. 손볼 것 ### P2 — "등록"이 아무것도 등록하지 않고 성공을 반환한다 - **사실.** `KafkaShareGroupRegistrar.register(profile, spec)`이 `spec`을 `Objects.requireNonNull`로만 처리하고 버린다. `ShareRegistration`은 `profile`과 `AtomicBoolean` 둘만 갖는다. Kafka 소비자가 만들어지지 않고(`import org.apache.kafka` 0건), `spec.sink`가 저장되지 않으므로 어떤 전달도 일어나지 않는다. 반환된 registration은 `isActive() == true`를 보고한다. - **근거.** `evidence/raw/290` §D·§E. - **왜 문제인가.** 같은 클래스의 javadoc이 pause를 조용히 무시하는 것을 거절한 이유로 "would let a retry policy that depends on pausing appear to work while doing nothing"을 든다. `register` 자체가 정확히 그 형태다 — 성공을 반환하고 아무것도 하지 않으며 `isActive()`가 true다. 오늘 호출자가 없으므로 사고는 아니지만, 이 leaf를 배선하는 사람이 가장 먼저 부를 메서드다. - **확인 방법.** `evidence/raw/290` §E 재실행. - **후보.** (a) 실제 share group 소비자를 만든다. (b) 미구현임을 명시하고 `MessagingCapabilityUnavailableException`으로 거절한다 — 이 leaf 자신의 원칙과 일관된다. (c) `register`를 제거하고 validator와 capability만 남긴다. - **다음 단계.** **CASE 후보.** "무시보다 거절"을 명시한 클래스가 자기 주 메서드에서는 무시한다는 형태가 그 자체로 가치가 있다. ### P3 — 선언된 의존 셋이 사용되지 않는다 - **사실.** `build.gradle`이 `org.apache.kafka:kafka-clients`를 선언하고 `import org.apache.kafka`가 0건. registry가 `messaging-policy`·`messaging-kafka` 의존을 허용하고 두 패키지의 import가 0건. - **근거.** `evidence/raw/290` §D. import 전수. - **왜 문제인가.** `verifyCleanArchitectureDependencies`는 `allowed_dependencies`를 **상한**으로 검사하므로 미사용 의존을 잡지 못한다. 결과: 이 leaf의 build closure가 실제 필요보다 넓고, `messaging-kafka`(34파일)와 그 전이 의존이 딸려 온다. 그리고 의존 선언이 "이 leaf가 Kafka를 쓴다"는 인상을 준다. - **확인 방법.** `grep -rn 'import org.apache.kafka\|import dev.caskeleton.messaging.policy\|import dev.caskeleton.messaging.kafka\.' src/messaging/messaging-kafka-share-experimental/src` - **후보.** 구현 전까지 미사용 의존을 제거하거나, 미완 상태임을 build.gradle 주석에 적는다. - **다음 단계.** **REFERENCE 후보**(허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다). ### P3 — 형제 어댑터 넷이 구현하는 SPI를 이 leaf만 구현하지 않는다 - **사실.** `KafkaMessagingTransport`·`RabbitMessagingTransport`·`PulsarMessagingTransport`·`NatsJetStreamTransport`가 전부 `MessagingTransport`를 구현한다. 이 leaf는 `TransportConsumerRegistration`만 부분 구현한다. - **근거.** `evidence/raw/280` §D(transport-spi probe)와 이 leaf의 소스. - **왜 문제인가.** `KafkaShareWorkQueueCapability`가 존재하는 이유("shared validators refuse … before a message is ever produced")가 실현되려면 `MessagingTransport.capabilities(DestinationName)`를 통해 값이 전달돼야 한다. 그 인터페이스를 구현하지 않으므로 capability는 아무도 읽지 않는 상수다. 두 experimental 형제(pulsar, nats)는 구현하므로 "experimental이라서"가 이유가 되지 않는다. - **확인 방법.** `git grep -n 'implements MessagingTransport' -- 'src/messaging/**/*.java'` - **후보.** `MessagingTransport`를 구현하거나, capability를 어떻게 전달할지 정한다. - **다음 단계.** **OPEN QUESTION 후보.** 판정이 §17 첫 항목("완성할 것인가")에 걸린다. ### P3 — 두 거절이 다른 예외 계층을 쓴다 - **사실.** `!enabled`는 `MessagingCapabilityUnavailableException`(안정 코드 `KAFKA_SHARE_DISABLED`), `orderingScope != NONE`은 `IllegalArgumentException`(코드 없음). - **근거.** `KafkaShareProfileValidator.java:31-40`. - **왜 문제인가.** 둘 다 설정 오류이고 둘 다 시작 시점에 잡힌다. 한쪽만 `FailureDescriptor`를 갖는다. `messaging-security`의 `MessageSecurityValidator`(전부 `IllegalArgumentException`)와 `BrokerTlsPolicy`(전부 `MessagingConfigurationException`)가 갈라진 것과 같은 형태다. - **확인 방법.** 두 throw 문 대조. - **후보.** 둘 다 `MessagingConfigurationException`으로 통일하고 안정 코드를 준다. - **다음 단계.** `messaging-security` §17의 같은 항목과 함께 **REFERENCE 후보**(구성 오류는 한 예외 타입과 안정 코드로 보고한다). ### P3 — 네 타입 중 하나만 테스트된다 - **사실.** `KafkaShareProfileValidatorTest`만 존재한다. registrar·capability에 테스트가 없다. - **근거.** `find src/test -name '*Test.java'` → 하나. - **왜 문제인가.** `register`가 spec을 버리는 것(§17 첫 항목)이 테스트가 있었다면 드러났을 형태다 — sink가 호출되는지 확인하는 테스트가 실패했을 것이다. capability 12개 boolean도 미검증이라 순서를 true로 바꿔도 아무것도 깨지지 않는다. - **확인 방법.** 테스트 클래스 목록. - **후보.** registrar와 capability에 테스트를 추가한다. - **다음 단계.** **REFERENCE 후보**(leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다) — `messaging-claim-check` §17과 같은 기준. ### P3 — 활성화 프로퍼티 키가 에러 메시지에만 존재한다 - **사실.** `backend.messaging.experimental.kafka-share=true`가 `KAFKA_SHARE_DISABLED` 메시지에 적혀 있다. 그 키를 읽는 코드가 저장소에 없다. - **근거.** `git grep -n 'kafka-share' -- src` → 이 leaf의 문자열 하나. - **왜 문제인가.** 운영자가 메시지를 보고 그 프로퍼티를 설정해도 효과가 없다. `enabled`는 `KafkaShareProfile` 생성자 인자이고 그 profile을 만드는 production 코드가 없다. - **확인 방법.** 키 문자열 검색. - **후보.** 배선될 때 프로퍼티 바인딩을 함께 만들거나, 메시지에서 키를 빼고 "이 profile의 `enabled`를 설정하라"로 바꾼다. - **다음 단계.** **REFERENCE 후보**(에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다) — `messaging-claim-check` §17의 "use claim check"와 같은 형태. ### 확인된 설계(문제 아님) - 순서 목적지를 degrade하지 않고 거절하는 것과 그 이유 - experimental을 기본 꺼짐으로 두는 것 - pause/resume을 조용히 무시하지 않고 실패 stage로 거절하는 것 - capability를 가정이 아니라 선언으로 두는 것 - `close()`가 멱등인 것 - 소비자 0·membership `[]`·조립 0의 삼중 정합 --- ## Source anchors | id | kind | path | revision | what it proves | limitations | |---|---|---|---|---|---| | MKS-001 | registry | `src/config/architecture/modules.json` | `21234e38` | deps 4개, `runtime_memberships: []` | 선언 | | MKS-002 | build | `messaging-kafka-share-experimental/build.gradle` | same | `kafka-clients` 선언 | 사용되지 않음(§12.4) | | MKS-003 | code | `.../kafka/share/KafkaShareProfileValidator.java` | same | §4.2 두 거절과 experimental 정책 | 예외 계층 불일치(§17) | | MKS-004 | code | `.../kafka/share/KafkaShareGroupRegistrar.java` | same | §4.3 spec 무시, §4.4 pause 거절 | 테스트 없음 | | MKS-005 | code | `.../kafka/share/KafkaShareProfile.java` | same | 다섯 필드와 두 거절 | `maxDeliveryCount` 미사용 | | MKS-006 | code | `.../kafka/share/KafkaShareWorkQueueCapability.java` | same | 12 boolean과 선언 목적 | 전달 경로 없음 | | MKS-007 | test | `KafkaShareProfileValidatorTest` (6) | same | 두 거절과 통과 | 네 타입 중 하나만 | | MKS-008 | cross-leaf code | 4개 `*MessagingTransport.java` | same | 형제 넷이 SPI 구현 | 각 leaf SSOT가 소유 | | MKS-009 | cross-leaf code | `messaging-policy/.../DestinationProfileValidator.java:78-87` | same | 연결되지 않은 두 순서 정책 | 해당 leaf SSOT가 소유 | | EVD-290 | command | `evidence/raw/290-claimcheck-and-kafkashare-unconsumed.txt` §C·§D·§E | same | §12.1·§12.4 | 정적 검색 | | EVD-293 | command | `./gradlew :messaging:messaging-kafka-share-experimental:test --rerun-tasks` | same | 6 / 0 / 0 | validator만 |