# 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만 |