Files
document-haness/docs/clean-architecture-backend-template/analysis/messaging/messaging-kafka-share-experimental.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

31 KiB

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에 있다.

// KafkaShareProfileValidator.java:10-17
 * <p>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.
 *
 * <p>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-policymessaging-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 — 두 거절

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을 받고 쓰지 않는다

public TransportConsumerRegistration register(
    KafkaShareProfile profile, TransportConsumerSpec spec) {
  Objects.requireNonNull(spec, "spec must not be null");
  validator.validate(profile);
  return new ShareRegistration(profile);
}

specnull 검사만 받는다. ShareRegistrationprofileAtomicBoolean active 둘만 갖는다.

TransportConsumerSpec(DestinationProfile profile, Function<TransportDelivery, CompletionStage<Void>> sink)이고, sink가 플랫폼이 전달마다 부르는 콜백이다(messaging-transport-spi §4.5). 그 sink가 저장되지 않으므로 어떤 메시지도 전달되지 않는다.

Kafka 소비자도 만들어지지 않는다 — kafka-clients를 import하는 코드가 없다.

register(...)아무것도 등록하지 않고 isActive() == true인 객체를 반환한다. §17.

4.4 ShareRegistration — pause/resume은 실패 stage

@Override
public CompletionStage<Void> pause(String scope) {
  return CompletableFuture.failedFuture(
      new MessagingCapabilityUnavailableException("KAFKA_SHARE_NO_PAUSE", ...));
}

registrar javadoc이 이유를 적는다.

// :12-15
 * <p>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<Void>이므로 비동기 계약을 지킨다. messaging-runtime-coreDefaultDeliveryProcessor.OneShotSettlement가 이중 정산을 failedFuture로 보고하는 것과 같은 규율이다.

이 거절이 messaging-policyRetryMode.PAUSE_PARTITION과 맞물린다 — 그 모드를 share group 목적지에 설정하면 DefaultRetryDecisionEnginePauseAndRetry를 고르고 이 registration이 그것을 거절한다. 두 leaf가 같은 사실을 양쪽에서 안다.

close()active를 false로 바꾸는 것 외에 아무것도 하지 않는다 — 해제할 자원이 없기 때문이다.

4.5 KafkaShareWorkQueueCapability — 12개 boolean

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.activeAtomicBoolean이다. close()set(false)이고 CAS가 아니므로 두 번 닫아도 무해하다(멱등).

KafkaShareProfileValidator·KafkaShareWorkQueueCapability는 상태가 없다. KafkaShareGroupRegistrar는 validator 참조 하나만 갖는다.

수명주기 참여 없음 — TransportConsumerRegistrationAutoCloseable이지만 이 구현은 닫을 자원을 갖지 않는다.


8. 설정·기능 플래그·환경 차이

항목
프로퍼티 키(에러 메시지에만 등장) backend.messaging.experimental.kafka-share
enabled KafkaShareProfile의 필드 — 호출자가 채운다

그 프로퍼티를 읽는 코드가 저장소에 없다. enabledKafkaShareProfile 생성자 인자이고 그 profile을 만드는 production 코드도 없다. 즉 키는 문서로만 존재한다. §17.

상수 없음.


9. 퍼시스턴스/외부 시스템 세부

없다. Kafka Share Group을 감싼다고 선언하지만 Kafka 클라이언트를 사용하지 않는다.

build.gradleimplementation '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를 반환하는 것, closeisActive를 바꾸는 것이 전부 미검증
  • 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-policyRetryContext.capabilitiesDefaultRetryDecisionEngine이 그 소비자인데, 그것에 이 값을 넘기는 경로가 없다. 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-kafkaKafkaMessagingTransport.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-apiMessagingCapabilityUnavailableException 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)specObjects.requireNonNull로만 처리하고 버린다. ShareRegistrationprofileAtomicBoolean 둘만 갖는다. 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.gradleorg.apache.kafka:kafka-clients를 선언하고 import org.apache.kafka가 0건. registry가 messaging-policy·messaging-kafka 의존을 허용하고 두 패키지의 import가 0건.
  • 근거. evidence/raw/290 §D. import 전수.
  • 왜 문제인가. verifyCleanArchitectureDependenciesallowed_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 — 두 거절이 다른 예외 계층을 쓴다

  • 사실. !enabledMessagingCapabilityUnavailableException(안정 코드 KAFKA_SHARE_DISABLED), orderingScope != NONEIllegalArgumentException(코드 없음).
  • 근거. KafkaShareProfileValidator.java:31-40.
  • 왜 문제인가. 둘 다 설정 오류이고 둘 다 시작 시점에 잡힌다. 한쪽만 FailureDescriptor를 갖는다. messaging-securityMessageSecurityValidator(전부 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=trueKAFKA_SHARE_DISABLED 메시지에 적혀 있다. 그 키를 읽는 코드가 저장소에 없다.
  • 근거. git grep -n 'kafka-share' -- src → 이 leaf의 문자열 하나.
  • 왜 문제인가. 운영자가 메시지를 보고 그 프로퍼티를 설정해도 효과가 없다. enabledKafkaShareProfile 생성자 인자이고 그 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만