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>
31 KiB
messaging-kafka-share-experimental 완전 해부
상태: COMPLETE 기준 revision:
21234e38cdb9a926cbc92bb97a2aee2e4a7d2916분석 범위:src/messaging/messaging-kafka-share-experimentalSSOT owner:messaging-kafka-share-experimentalintegration/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-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 — 두 거절
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);
}
spec은 null 검사만 받는다. ShareRegistration은 profile과 AtomicBoolean 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-core의 DefaultDeliveryProcessor.OneShotSettlement가 이중 정산을 failedFuture로 보고하는 것과 같은 규율이다.
이 거절이 messaging-policy의 RetryMode.PAUSE_PARTITION과 맞물린다 — 그 모드를 share group 목적지에 설정하면 DefaultRetryDecisionEngine이 PauseAndRetry를 고르고 이 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.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이 순서와 양립 불가인 이유 —
KafkaShareProfileValidatorjavadoc - experimental이 기본 꺼짐인 이유 — 같은 javadoc
- pause/resume을 무시하지 않고 거절하는 이유 —
KafkaShareGroupRegistrarjavadoc - capability를 선언으로 두는 이유 —
KafkaShareWorkQueueCapabilityjavadoc - 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.kafka0건),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만 |