Files

10 KiB

Messaging 지원 매트릭스

플랫폼이 무엇을 보장하는지무엇을 보장하지 않는지를 브로커별로 고정한다. 여기 없는 조합은 지원되지 않는다.

등급은 증거를 따른다. CompatibilityMatrix.Entry.hasLiveBrokerCertification()은 선언된 boolean이 아니라 CertifiedEvidence가 가진 레인 증거에서 파생된다. RabbitMQ가 Stable에서 내려온 이유가 이것이다 — 어댑터는 공유 contract 7개를 통과하고 RabbitBrokerIT가 실 컨테이너에서 정상 경로를 돌리지만, 이 저장소의 Stable 기준인 장애 시나리오 증거가 하나도 없다. 레인이 생겨 증거를 내면 등급은 코드 수정 없이 따라 올라간다.

인증 근거. 이 표의 버전은 이 저장소의 컨테이너 레인이 실제로 실행한 이미지다. 이전 판은 Kafka 4.2/4.3을 선언했지만 fixture는 apache/kafka:4.1.0, lockfile client는 4.1.1이었다 — 표와 코드 상수가 서로 일치했을 뿐 어느 쪽도 실행된 적이 없었다. 장애 시나리오 커버리지도 마찬가지로 BrokerFailureMatrix.shipped() 하드코딩이 아니라 레인이 낸 증거(BrokerCertificationEvidence)에서 나온다. 증거가 없는 조합은 NOT_COVERED다 (MSG-014).

모듈 이름과 런타임 편입. messaging-outbox-jdbc-postgresql / messaging-inbox-jdbc-postgresql은 이전에 -jpa로 불렸다. 구현은 Spring JDBC이고 SQL은 PostgreSQL 전용(?::jsonb, FOR UPDATE SKIP LOCKED, ON CONFLICT, TIMESTAMPTZ)이므로, 그 이름은 쓰지 않는 기술을 광고하고 vendor 중립 port(messaging-reliability-api)의 위치를 가렸다 (MSG-023).

또한 registry의 messaging leaf는 모두 runtime_memberships가 비어 있다. 이는 build-only / incubating — 어느 composition root에도 편입되지 않았다는 뜻이며, 아래의 등급과는 다른 축이다. 등급은 "무엇이 증명되었는가", membership은 "무엇이 실행되는가"를 말한다. 애플리케이션에 배선하려면 registry를 먼저 바꾸고 verifyRuntimeModuleMembership을 통과시켜야 한다. 자세한 규칙은 src/messaging/CLAUDE.md가 소유한다.

브로커 등급

브로커 등급 인증 기준 Stable 기능 제한
Kafka Stable 4.1.x producer idempotence, consumer group, batch, pause/resume, replay, transaction capability Share Group은 Experimental
RabbitMQ Experimental 4.3.x exchange/routing, publisher confirm, mandatory return, manual ACK, quorum queue, retry queue, DLQ 장애 시나리오 레인 미실행 — 증거 없음. stream 및 특수 plugin 미지원
Pulsar Experimental 4.0 LTS + 4.2 typed publish/consume, Shared, Key_Shared, schema transaction 미승격, 기본 비활성
NATS JetStream Experimental 2.14.x stream, durable consumer, explicit ACK, dedupe, replay native DLQ 없음(플랫폼이 대행), 기본 비활성
Artemis/JMS Extension 범위 밖 adapter SPI만 별도 ADR + Contract Suite 통과 필요

Capability 매트릭스

MessagingCapabilities가 런타임에 선언하는 값이다. false인 기능을 요구하는 destination profile은 startup에서 실패하며, 조용히 약화되지 않는다.

Capability Kafka Kafka Share RabbitMQ Pulsar NATS JS
brokerAcknowledgement O O O O O
replicationOrPersistenceEvidence O O O O O
perMessageSettlement O O O O O
batchSettlement O X X O O
orderedStream O X X X O
keyedOrdering O X X Key_Shared만 X
replay O X X O O
delayedDelivery X X retry queue로 대행 O X
brokerTransaction O X X 미승격 X
deduplicatedPublish O X X X O
nativeDeadLetter X X O O X
topologyManagement O X O O O

Kafka Share Group이 ordering 전부 X인 것은 설계 결정이다. share group은 개별 record를 경쟁 소비자에게 나눠주고 개별 ack하므로 partition 순서를 유지할 수 없다. ordered destination을 share group에 설정하면 KafkaShareProfileValidator가 거부한다.

NATS JetStream의 nativeDeadLetter=X도 마찬가지다. JetStream은 delivery limit 초과 시 메시지를 terminate할 뿐 어디로도 라우팅하지 않으므로, 플랫폼이 DLQ publish를 직접 수행한다.

기능 등급

기능 등급
Typed Publish·Consume Stable M1
At-least-once contract Stable
Ambiguous publish 결과 Stable
handler 성공 후 자동 settlement Stable M1
Batch / Manual settlement / Pause·Resume / Delayed / Replay 요청 M2
Broker transaction / partition / routing / subscription M3
Replay 실행 / Redrive / offset reset / purge / delete M4 Admin
Kafka Share Group, Pulsar, NATS Experimental
Spring Cloud Stream bridge Optional

무엇이 "Stable"을 증명하는가

Stable 등급은 두 가지를 모두 통과해야 한다. CompatibilityMatrixTest가 이 규칙을 강제한다.

1. 공유 Contract Suite (MessagingAdapterContract, 7개)

Kafka와 RabbitMQ가 동일한 7개 테스트를 변경 없이 통과한다. 결정적 하네스를 쓰므로 확인 유실·settlement 유실 같은 장애를 요청 시점에 재현할 수 있다.

2. 실 브로커 인증 (Testcontainers)

스위트 무엇을 증명하는가
KafkaBrokerIT acks=all이 실제 replication 증거를 만든다 / 잘못된 토픽은 REJECTED / 발행-소비 왕복에서 identity 보존 및 contiguous commit
KafkaAmbiguityChaosIT 브로커를 docker pause로 멈춘 상태의 publish가 AMBIGUOUS 로 보고된다 (broker acceptance 없음, confirmation level NONE, 비-retryable)
KafkaBrokerCertificationIT 인증 레인. Toxiproxy를 broker 앞에 두고 connection cut / confirm 유실 / 지연 / settlement 유실을 각각 주입하고, 통과한 시나리오마다 BrokerCertificationEvidence 한 줄을 manifest에 쓴다
RabbitBrokerIT exchange가 confirm했는데 어떤 큐에도 바인딩되지 않은 publish가 REJECTED + UNROUTABLE 로 보고된다
OutboxPostgresIT 롤백된 트랜잭션은 발행 가능한 행을 남기지 않는다 / SKIP LOCKED lease가 두 relay를 분리한다 / ambiguous 행이 같은 messageId로 재클레임된다
InboxPostgresIT 재전달이 side effect를 두 번 적용하지 않는다 / 롤백은 예약도 되돌린다

Docker가 없으면 DockerAvailability 가드로 skip되며, 이 표의 항목은 그때 검증되지 않은 것으로 취급한다. KafkaBrokerCertificationIT만 예외다 — 인증 레인은 가드를 달지 않고 Docker가 없으면 실패한다. skip하는 레인은 아무도 켜지 않은 브로커에 대해 성공을 보고하기 때문이다. 그래서 이 레인은 test에서 태그로 제외되고 messagingCertificationTest로만 실행된다.

3. 장애 시나리오 커버리지 (BrokerFailureMatrix)

NetworkFaultScenario가 5개 시나리오와 각각의 기대 결과를 코드로 고정한다. 기대 결과를 어댑터별로 두지 않는 것이 핵심이다 — 어댑터마다 다른 답을 허용하면 공유 계약이 존재할 이유가 없다.

시나리오 시점 기대 결과 이유
connection-refused 전송 전 REJECTED 바이트가 나가지 않았으므로 broker가 가질 수 없다
connection-cut-after-write 전송 후 AMBIGUOUS broker가 저장했고 confirm만 유실됐을 수 있다
confirm-timeout 전송 후 AMBIGUOUS timeout은 부재의 증거가 아니라 증거의 부재다
settlement-lost settlement 중 REDELIVERED 미settlement 메시지는 재전달이 설계다
high-latency 전송 후 AMBIGUOUS 판단 시점에는 confirm 유실과 구별할 수 없다

CrossBrokerContractSuite가 릴리스 게이트로 이를 강제한다. Stable 어댑터는 5개 전부를 실 브로커에서 커버해야 하고, Experimental 어댑터는 LIVE_BROKER 커버리지를 주장할 수 없다. 커버리지는 능력이 아니라 무엇을 실제로 돌렸는지의 기록이다.

증거의 출처. CertifiedEvidence는 더 이상 손으로 쓴 목록이 아니라 messaging-testkit/src/main/resources/messaging/broker-certification-evidence.jsonl을 읽는다. 그 파일은 messagingCertificationTest 레인이 실제 Kafka 컨테이너에 장애를 주입하며 만들어낸 출력이고, verifyMessagingCertificationEvidence가 커밋된 manifest와 이번 실행의 출력을 대조해 다르면 빌드를 실패시킨다. 즉 manifest를 손으로 고치면 게이트가 깨지고, 레인을 돌리면 manifest가 다시 쓰인다.

오늘 Kafka가 가진 증거는 connection-cut-after-write · confirm-timeout · high-latency · settlement-lost 네 개다. connection-refused는 남은 gap이며 그 이유가 있다 — Kafka producer는 연결 존재 여부를 알기 전에 레코드를 버퍼에 넣으므로, 연결 거부는 전송에 대해 아무것도 증명하지 못하는 delivery timeout으로 나타난다. 이를 REJECTED로 보고하는 것은 이 플랫폼이 금지한 추측이므로, 시나리오는 CertifiedEvidence.knownGaps가 이름으로 들고 있는 미커버 항목으로 남는다.

실 브로커가 실제로 잡아낸 결함

이 스위트들은 장식이 아니다. 작성 과정에서 결정적 테스트가 통과하는데 실 인프라에서 실패한 결함을 두 건 잡았다.

  1. Outbox IN_FLIGHT 고아 행 — lease 쿼리가 PENDING/AMBIGUOUS만 클레임 대상으로 봐서, publish 도중 죽은 relay가 남긴 행이 lease 만료 후에도 영영 회수되지 않았다.
  2. Rabbit confirm 경합 — transport가 publish 후에 confirm을 등록해서, 연결 스레드에서 confirm이 먼저 도착하면 유실되고 호출자가 무한 대기했다.

둘 다 인메모리 double이 실제보다 관대해서 통과하고 있었다.

명시적 비지원

  • 공통 EXACTLY_ONCE 설정 — DeliveryGuarantee에 상수가 존재하지 않는다.
  • 전역 순서 — OrderingScopeGLOBAL이 존재하지 않는다.
  • DB와 broker의 자동 원자 transaction, 기본 XA
  • Java native serialization
  • 무제한 payload·header, 무한 retry
  • 운영 application에서의 topology 파괴 작업
  • 일반 애플리케이션에 raw broker client 반환
  • DLQ publish 확인 전 source ACK