# messaging-rabbit 완전 해부 > 상태: COMPLETE > 재오픈 게이트: cycle 2 재통독(2026-09-01) — `src/main` production 20파일 2,443줄 + `src/test` 10파일 1,727줄 축자 통독 완료. `STRUCTURAL_ONLY` 는 `gradle.lockfile` 하나. > 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916` > 분석 범위: `src/messaging/messaging-rabbit` > SSOT owner: `messaging-rabbit` > integration/family document: `analysis/19-messaging-platform.md` (secondary, INTEGRATION_ONLY) --- ## 0. SSOT identity / 커버리지 - `runtime_memberships`: **`["app-bootstrap"]`** — 클래스패스에 올라간다 - 도달성: **없다.** 제공자 선택이 `rabbit` 을 이름으로 거부한다(§12.1) | 파일 | LOC | 참조 | |---|---:|---| | `RabbitConfirmCoordinator` | 279 | 전송 + 테스트 | | `RabbitMessagingTransport` | 246 | 테스트만 | | `RabbitConsumerRegistrar` | 238 | 테스트만 | | `RabbitDeliveryMapper` | 195 | 소비자 + 테스트 | | `RabbitHeaderMapper` | 180 | 매퍼 둘 + 테스트 | | `RabbitSecurityConfigurer` | 179 | 스타터 빈만 — 호출처 없음 | | `RabbitBatchConsumerRegistrar` | 165 | 테스트만 | | `RabbitTopologyProfile` | 137 | 네이티브 DLQ 능력 + 테스트 | | `RabbitDeadLetterPublisher` | 130 | **자기 파일 밖 참조 0** | | `RabbitPublishFailureClassifier` | 117 | 전송 + 테스트 | | `RabbitSettlementController` | 84 | 소비자 + 테스트 | | `RabbitProfileValidator` | 83 | 스타터 시작 검증 | | `RabbitPublishMapper` | 72 | 전송 | | `RabbitNativeDeadLetterCapability` | 70 | DLQ 발행자 + 테스트 | | `RabbitRetryQueueTopology` | 57 | 테스트만 | | `RabbitSettlementOperations` | 50 | 인터페이스 — 구현은 테스트 셋뿐 | | `RabbitBrokerProfile` | 50 | 설정 컴파일 | | `RabbitChannelPublisher` | 42 | 인터페이스 — **production 구현 0** | | `RabbitPublishReference` | 37 | 좌표 | | `RabbitRequestReply` | 32 | **자기 파일 밖 참조 0** | main 총 **20파일 / 2,443줄**. ### Coverage ledger | scope | count | disposition | reason | |---|---:|---|---| | `main/java/**` | 20 | `FULL_READ` | 2,443줄. 위 표가 전부 | | `test/java/**` | 10 | `FULL_READ` | 1,727줄 | | `build.gradle` | 1 | `FULL_READ` | 전문 | | `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일 — 생성물 | `UNCLASSIFIED` 0. > "참조" 열은 2026-09-01 재통독에서 저장소 전체 grep 으로 채웠다. 이전 판의 Source anchors 는 절반을 괄호 하나로 묶어 두었고, 그 괄호 안에 §17.2·§17.3·§17.4 가 있었다. --- ## 1. 이 어댑터의 중심 — 확인과 반환은 다른 질문에 답한다 > "AMQP delivers a return *before* the confirm for an unroutable message, so a naive adapter that > completes on the confirm reports success for a message the broker threw away. The coordinator > therefore keeps each publish pending until the confirm arrives, and remembers whether a return was > seen first." 네 결과가 나온다 — 확인+미반환은 `CONFIRMED`, 확인+반환은 `UNROUTABLE` 로 `REJECTED`, 부정 확인은 `REJECTED`, 확인 미도착은 `AMBIGUOUS`. 이 리프에서 가장 중요한 판단이고, 실브로커 시험(`RabbitBrokerIT.anUnroutablePublishIsRejectedEvenThoughTheExchangeConfirmedIt`)이 그것을 붙든다. 그리고 그 시험이 production 에 없는 조각을 스스로 채워 넣는다(§17.2). ## 2. 자료구조 선택이 결함 수정이다 ```java private final ConcurrentSkipListMap pending = new ConcurrentSkipListMap<>(); ``` > "Ordered because a Rabbit confirm carries a `multiple` flag meaning 'everything up to and including > this tag'. Resolving one sequence per confirm — which is what a hash map forces — leaves every > earlier publish pending forever: the caller's stage never completes and the entry is never removed, > so the map grows for the life of the connection." `confirmed(sequence, multiple=true, …)` 가 `headMap(sequence, true)` 로 범위를 해소한다. 전용 시험이 있다. ## 3. 부정 확인의 증거를 전송됨으로 기록한다 ```java // A NACK is the broker's answer to a frame it received. Recording it as never sent contradicts the // very evidence that produced it, and a caller reading the evidence would conclude the message can // be re-sent freely. ``` `REJECTED` + `TRANSMITTED` + `ConfirmationLevel.NONE`. 완결 상태와 전송 증거를 분리해서 다루는 곳이 이 가족에서 여기와 Kafka 뿐이다. ## 4. 소비·정착·죽은 편지의 세 규율 **좁은 catch.** `RabbitConsumerRegistrar.onMessage` 가 디코딩만 감싸는 안쪽 `try` 를 따로 둔다. > "One catch around decode, the handler and the settlement meant a business failure or an ACK that > could not be written was recorded as an undecodable payload and discarded — a message that should > have been retried, deleted instead." **정착하지 않은 핸들러.** 완료했는데 정착하지 않으면 대신 ack 하지 않고 requeue 한다 — "acknowledging on its behalf would silently drop it". **네이티브 죽은 편지.** `RabbitNativeDeadLetterCapability` 가 두 조건을 모두 요구한다. > "If the dead-letter exchange is unroutable — nobody bound a queue to it, or the binding was removed > — the broker discards the message silently and the reject still succeeds." ## 5. 자격증명은 연결 시도마다 해석된다 > "RabbitMQ client connections are long-lived and reconnect on their own, so a factory holding a > credential from startup will happily reconnect with a revoked one for as long as the process runs — > the reconnect is exactly the moment a rotated credential should take effect." `AmqpCredentials` 가 record 가 아니라 class 인 이유도 적혀 있다 — 비밀을 지우려면 가변이어야 하고, record 가 `char[]` 를 동등성에 쓰면 같은 자재를 가진 둘이 서로 다르다고 판정된다. ## 6. 시작 검증 `RabbitProfileValidator` 가 여덟을 요구한다 — Stable 에 확인·반환·mandatory, 소비자 auto-ack 금지, prefetch ≥ 1, 확인 마감 양수, 운영에 TLS·인증. 그리고 목적지 검증이 둘 더 — 작업 큐에 쿼럼 큐, 교환기나 큐 중 하나. Kafka 쪽과 달리 이 검증기는 스타터에서 `StartupProfileValidation` 으로 **감싸여 있다**. 다만 그 자동 설정 자체가 도달하지 않는다(§12.1). ## 10. 테스트 레인 10파일 1,727줄. | 파일 | 줄 | 무엇을 붙드나 | |---|---:|---| | `RabbitRuntimeTest` | 309 | 전송 4경로 + 소비자 7경로(일시정지·배수·미디코딩·핸들러 실패·미정착·close) | | `RabbitContractHarness` | 304 | 공유 어댑터 계약을 production 조정자·정착 제어기 위에서 | | `RabbitBrokerIT` | 232 | 실브로커 `rabbitmq:4.3-management` — 반환-먼저-확인 | | `RabbitTopologyAndBatchTest` | 218 | 쿼럼 요구·DLX 논리·실패 분류·배치 누적 | | `RabbitProfileValidatorTest` | 184 | 검증기 여덟 규칙 | | `RabbitConfirmCoordinatorTest` | 167 | 상태 기계 12경로(다중 확인·채널 종료 포함) | | `RabbitEnvelopeRoundTripTest` | 152 | 헤더 왕복·위조 거부 | | `RabbitSettlementControllerTest` | 105 | 일회 종결·지연 재시도 큐 인자 | | `RabbitAdapterContractTest` · `RabbitFixtureProfiles` | 24 · 32 | 계약 실행·픽스처 | `RabbitAdapterContractTest` 의 javadoc 이 이 레인의 요점을 적는다. > "Two brokers with completely different machinery — offsets and commits versus delivery tags and > confirms — answering the same seven questions the same way is what makes the logical destination > abstraction real rather than aspirational." ## 12. negative-space probes **12.1 도달성 — 리프 전체가 production 호출자를 갖지 않는다.** 전송을 만들려면 `RabbitChannelPublisher` 구현이 필요하다. 저장소 전체에서 그 인터페이스의 구현은 **테스트의 익명 클래스 둘**(`RabbitRuntimeTest:49`, `RabbitBrokerIT:709`)뿐이다. 따라서 `new RabbitMessagingTransport(...)` 도 테스트에만 있고, `RabbitConsumerRegistrar`·`RabbitBatchConsumerRegistrar` 도 마찬가지다. 그리고 그 사실이 플랫폼 쪽에 이름으로 기록되어 있다. ```java // MessagingProviderSelection static final Map BROKERS_WITHOUT_A_TRANSPORT = Map.of( "rabbit", "the Rabbit adapter ships its validators and security configuration but no MessagingTransport: " + "its native channel publisher is not implemented, so a publish has nothing to travel on"); ``` `app.messaging.broker=rabbit` 은 시작 오류이고, 전용 시험이 그 메시지를 단언한다. 그래서 `RabbitMessagingAutoConfiguration` 98줄도 도달하지 않는다. 이 리프의 품질과 도달성이 정반대다. 코드는 이 가족에서 가장 정교한 축이고 — 반환-먼저-확인 상태 기계, `multiple` 범위 해소, 정착 일회성, 네이티브 DLQ 의 조건부 신뢰 — 실행 경로는 없다. **12.2 리프 자체 기준으로도 죽은 둘.** | 파일 | LOC | 상태 | |---|---:|---| | `RabbitDeadLetterPublisher` | 130 | 자기 파일 밖 참조 0 — production 도 테스트도 부르지 않는다 | | `RabbitRequestReply` | 32 | 인터페이스. 구현 0, 테스트 0, 호출 0 | `RabbitDeadLetterPublisher` 가 담고 있는 것이 §4 의 세 번째 규율 — 네이티브 경로와 플랫폼 발행 중 어느 쪽을 쓸지 한 곳에서 결정한다는 판단 — 인데, 그 결정을 내리는 코드를 아무도 부르지 않는다. 그 판단의 근거가 되는 `RabbitNativeDeadLetterCapability` 는 테스트가 있다. 즉 **판단의 재료는 시험되고 판단 자체는 시험되지 않는다.** `RabbitRequestReply` 는 M2 능력의 인터페이스 선언이다. javadoc 이 왜 제한적으로 제공하는지를 적는데("a synchronous call wearing an asynchronous costume"), 제공되는 것이 없다. **12.3 대조군 — 재시도 헤더 오염의 처리가 두 어댑터에서 갈린다.** 두 어댑터의 `attemptOf` 는 같은 fail-closed 결정을 같은 문구로 적는다. ```java // "the message is quarantined rather than restarting its retry budget" throw new MessagingConfigurationException("RETRY_ATTEMPT_MALFORMED", …); ``` 그런데 소비자가 그 던짐을 받는 위치가 다르다. | 어댑터 | `attemptOf` 호출 위치 | 결과 | |---|---|---| | Rabbit | `toMetadata` 안 → 디코딩 실패 `catch` 안쪽 | `operations.discard(tag, …)` — DLX 가 있으면 죽은 편지로 | | Kafka | 디코딩 `catch` **바깥**의 두 번째 블록 | `requeueAfterFailure()` → 무한 pause-and-seek(messaging-kafka §17.4) | 같은 판단, 반대 결과다. Rabbit 쪽이 javadoc 이 약속한 것에 가깝다. **12.4 대조군 — `pause` 의 뜻이 SPI 하나 뒤에서 두 가지다.** | 어댑터 | 반환 시점 | 의미 | |---|---|---| | Kafka | 다음 폴 주기 | `consumer.pause()` — 브로커에서 더 가져오지 않는다 | | Rabbit | 즉시 완료 | `onMessage` 가 `false` 를 답한다 — 리스너 컨테이너가 계속 밀고, 미확인으로 재배달된다 | Kafka 쪽 javadoc 은 왜 즉시 완료하지 않는지를 명시한다("'paused' cannot be true until the loop says so"). Rabbit 쪽에는 그 대비 서술이 없다. §17.4. **12.5 드리프트.** 검증기가 강제하는 항목과 어댑터가 실제로 보내는 플래그(`mandatory`)가 일치한다. ## 16. 확인하지 못한 것 - 실제 브로커로 반환-먼저-확인 순서를 재현하지 않았다. `RabbitBrokerIT` 가 그 레인이고 컨테이너가 필요하다. - 지연 재시도 큐 토폴로지를 실제로 선언해 보지 않았다. - §17.2 를 실행으로 재현하지 않았다. `RabbitHeaderMapper.toProperties` 전문에 순번 헤더가 없다는 것과, `RabbitBrokerIT` 가 자기 publish 람다에서 `x-seq` 를 붙인다는 것으로 판정했다. - `RABBIT-CR-DEMO`(§17.3)를 실제 브로커에 붙여 보지 않았다. 이름과 RabbitMQ 의 기본 활성 상태로 판정했다. - `gradle.lockfile` 은 읽지 않았다(`STRUCTURAL_ONLY`). ## 17. 손볼 것 ### 17.1 P3 — 확인 등급이 요구에서 파생되고, 그 요구를 뒷받침하는 강제는 목적지 종류 하나에만 걸린다 ```java ConfirmationLevel level = requirement == ConfirmationRequirement.REPLICATION_OR_PERSISTENCE_ACK ? ConfirmationLevel.REPLICATION_OR_PERSISTENCE_ACK : ConfirmationLevel.BROKER_ACK; ``` 증거의 등급이 브로커가 무엇을 했는지가 아니라 프로파일이 무엇을 **요구했는지** 에서 나온다. 대부분의 경우 이 파생은 성립한다. 두 강제가 그것을 받쳐 준다. - `RabbitHeaderMapper.toProperties` 가 배달 모드를 무조건 `PERSISTENT` 로 둔다. RabbitMQ 는 지속 메시지를 디스크에 쓴 뒤에 확인한다. - `RabbitProfileValidator.validateDestination` 이 내구 작업 큐에 쿼럼 큐를 요구한다. 쿼럼 큐의 확인은 다수 복제 뒤에 온다. 빈틈은 둘째 강제의 범위다. ```java if (destination.kind() == DestinationKind.WORK_QUEUE && !broker.quorumQueues()) { throw …; } ``` 작업 큐가 아닌 목적지에는 쿼럼 요구가 없다. 교환기로 발행하는 목적지가 `REPLICATION_OR_PERSISTENCE_ACK` 를 요구하면, 그 교환기에 바인딩된 큐가 고전 큐여도 어댑터는 그 등급을 보고한다. 지속 모드 덕분에 디스크 기록은 보장되지만 복제는 보장되지 않는다. 이 저장소의 규율은 증거가 관측에서 나와야 한다는 것이다 — `MessagingCapabilities` 의 javadoc 이 "a silently weakened guarantee is indistinguishable from a working one until the incident" 라고 적는다. 수정은 쿼럼 요구를 목적지 종류가 아니라 **요구된 확인 등급** 에 걸거나, 작업 큐가 아닌 목적지에서는 등급을 `BROKER_ACK` 로 낮추는 것이다. ### 17.2 P2 — 반환을 순번에 맞추는 조각이 production 에 없고, 시험이 그 자리를 스스로 메운다 이 어댑터의 핵심 보장(§1)은 반환과 확인을 **같은 발행** 에 묶는 데 달려 있다. 묶는 열쇠는 순번이다. ```java public void returned(long sequence) { … } // RabbitConfirmCoordinator public void onReturn(long sequence) { … } // RabbitMessagingTransport ``` 그런데 AMQP 의 `basic.return` 콜백은 순번을 주지 않는다. 교환기·라우팅 키·속성·본문만 온다. 그래서 발행자가 순번을 메시지에 실어 보내고 반환에서 되읽어야 한다. `RabbitHeaderMapper.toProperties` 전문에 그런 헤더가 없다. 쓰는 것은 `msg.*` 예약 헤더들과 AMQP 의 `messageId`·`correlationId`·`timestamp`·`deliveryMode` 뿐이다. 그 조각이 존재하는 곳은 시험 하나다. ```java // RabbitBrokerIT channel.addReturnListener(returned -> transport.onReturn(Long.parseLong(returned.getProperties().getHeaders().get("x-seq").toString()))); … private static Map withSequence(MessageProperties source, long sequence) { headers.put("x-seq", Long.toString(sequence)); // ← 시험이 직접 붙인다 } ``` 그 메서드의 javadoc 이 문제를 정확히 서술한다. > "A returned message arrives without its publish sequence number, so the adapter has to carry one > itself to correlate the return with the pending publish." "the adapter has to" 인데 어댑터는 하지 않는다. `RabbitChannelPublisher` 의 javadoc 은 **등록 경합**(확인이 `basicPublish` 반환보다 먼저 올 수 있다)만 설명하고 이 상관 문제는 언급하지 않는다. 결과는 이렇다. 언젠가 `RabbitChannelPublisher` 를 구현하는 사람은 이 헤더 규약을 다시 발명해야 하고, 발명하지 않으면 `onReturn` 이 호출되지 않아 unroutable 발행이 **`CONFIRMED` 로 보고된다** — 이 어댑터가 존재하는 이유로 든 바로 그 실패다. 수정은 순번 헤더를 `RabbitHeaderMapper` 나 `RabbitPublishMapper` 로 올려 production 계약으로 만들고, 그 이름을 `RabbitChannelPublisher` javadoc 에 적는 것이다. 지금은 그 규약이 시험 파일 20줄에만 있다. ### 17.3 P3 — SCRAM 자격을 RabbitMQ 의 데모 기구로 조용히 매핑한다 ```java case BrokerCredentialProfile.SaslScram scram -> { CredentialRuntime resolved = credentials.resolve(scram.credentialId(), now); yield new AmqpCredentials("RABBIT-CR-DEMO", scram.credentialId(), resolved.material(), profile.tlsEnabled()); } ``` `RABBIT-CR-DEMO` 는 RabbitMQ 의 시연용 challenge-response 인증 기구(`rabbit_auth_mechanism_cr_demo`)의 이름이고 기본 활성이 아니다. RabbitMQ 는 SCRAM-SHA 를 구현하지 않으므로 `SaslScram` 에 대응하는 AMQP 기구가 없다는 것 자체는 사실이다. 문제는 그 사실을 다루는 방식이 같은 파일 안에서 일관되지 않다는 것이다. ```java case BrokerCredentialProfile.Nkey ignored -> throw new IllegalArgumentException("NKey credentials are a NATS concept, not an AMQP one"); // ← 거부 case BrokerCredentialProfile.OAuth2 oauth -> { // RabbitMQ's OAuth 2 plugin takes the token in the password field of a PLAIN exchange. yield new AmqpCredentials("PLAIN", …); // ← 주석으로 근거 } case BrokerCredentialProfile.SaslScram scram -> yield new AmqpCredentials("RABBIT-CR-DEMO", …); // ← 둘 다 없다 ``` 그리고 이웃 어댑터의 같은 클래스가 정확히 이 상황에 대한 규범을 적어 두었다. > "Refused rather than half-configured. Setting the mechanism name without a callback handler > produces a client that authenticates with nothing and fails at connect time, which is later and > harder to attribute than failing here." — `KafkaSecurityConfigurer` `SaslScram` 에도 그 규범이 적용되어야 한다. 플러그인이 없는 브로커에서는 handshake 가 알아보기 어려운 오류로 실패하고, 있는 브로커에서는 시연용 기구로 인증한다. 수정은 `Nkey` 와 같이 거부하거나, `PLAIN` 으로 매핑하고 그 이유를 주석으로 남기는 것이다. 어느 쪽이든 지금처럼 말없이 데모 기구를 고르는 것보다 낫다. ### 17.4 P3 — 능력 상수의 `delayedDelivery` 가 무조건 참이고, 그 지연을 제공할 토폴로지는 조립되지 않는다 ```java private static final MessagingCapabilities CAPABILITIES = new MessagingCapabilities(true, true, true, false, false, false, false, true, false, false, true, true); // ^^^^ delayedDelivery ``` 이 플래그는 읽힌다. ```java // DefaultRetryDecisionEngine:64 if (policy.mode() == RetryMode.BROKER_DELAYED && context.capabilities().delayedDelivery()) { … } ``` 그런데 지연을 실제로 만드는 것은 `RabbitRetryQueueTopology` 이고, 그 클래스는 자기 파일과 시험 하나 밖에서 참조되지 않는다. 어떤 production 코드도 그 큐를 선언하지 않는다. 그리고 그 클래스의 javadoc 이 이 지연의 성질을 정확히 적는다. > "TTL expiry is evaluated at the head of the queue, so mixed delays in one retry queue do not expire > independently." 즉 제공되는 것은 "메시지별 지연" 이 아니라 "재시도 큐 하나당 TTL 하나" 다. 능력 모델에는 그 구분을 표현하는 자리가 없고, 상수는 프로파일과 무관하게 참을 답한다. Kafka 는 같은 칸을 `false` 로 둔다. 그래서 이 플래그의 두 값이 "지연 있음/없음" 이 아니라 "지연을 흉내낼 토폴로지를 선언할 수 있음/없음" 을 뜻하게 된다. 수정은 능력을 전송 상수가 아니라 목적지의 재시도 큐 선언에서 파생시키는 것이다. 이 리프가 조립되지 않는 동안에는 P3 이고, `RabbitChannelPublisher` 구현이 생기는 날 함께 봐야 한다. ### 17.5 P3 — `pause` 의 의미가 SPI 하나 뒤에서 두 브로커에 다르게 구현된다 ```java @Override public CompletionStage pause(String scope) { pausedScopes.add(scope == null ? "" : scope); return CompletableFuture.completedFuture(null); // ← 즉시 완료 } ``` 호출자가 이 단계를 기다리고 나면 "일시정지되었다" 고 읽는다. 실제로 일어난 것은 `onMessage` 가 이후 배달에 `false` 를 답하기 시작한 것뿐이고, 리스너 컨테이너는 계속 배달을 밀며 그 배달들은 미확인 상태로 재배달된다. 즉 정지가 아니라 거부-재배달 루프다. Kafka 쪽은 같은 SPI 를 정반대로 구현하고 그 이유를 적는다. > "The returned stage completes after the poll loop has actually applied the change, so a caller that > awaits it knows the consumer is paused rather than merely asked to pause… there is no safe way to > touch the consumer from another thread, so 'paused' cannot be true until the loop says so." AMQP 에는 대응하는 수단이 있다 — `basicCancel` 로 소비자를 취소하거나 컨테이너를 멈추는 것. 지금 구현이 그것을 하지 않는 이유는 어디에도 없다. 전용 시험(`aPausedQueueRefusesDeliveriesSoTheBrokerRedeliversThem`)의 이름이 이미 실제 동작을 정확히 말한다. 그러므로 수정은 둘 중 하나다 — 컨테이너를 실제로 멈추거나, SPI 의 javadoc 에 "브로커에 따라 정지가 거부-재배달일 수 있다" 를 명시하는 것. ### 확인된 설계(문제 아님) - **확인과 반환을 두 질문으로 나누고, 반환-먼저 순서를 상태 기계로 다룬 것.** - **정렬된 맵을 골라 `multiple` 확인의 범위 해소를 가능하게 한 것과, 해시 맵이 만들었을 누수를 javadoc 에 남긴 것.** - **부정 확인의 증거를 전송됨으로 기록한 것과 그 근거.** - **채널 종료를 모호로 완결시킨 것** — 보류로 남기면 호출자가 매달린다. - **순번 예약을 발행과 분리한 것** — 확인이 `basicPublish` 반환을 앞지를 수 있다. - **동기 발행 실패를 던지지 않고 분류기를 거쳐 스테이지로 돌려주는 것.** - **적재물 크기와 종료 상태를 채널 앞에서 검사해 미전송 증거로 실패시키는 것.** - **정착의 일회성과 재사용된 배달 태그의 위험을 명시한 것.** - **디코딩만 감싸는 좁은 `catch`** — 넓은 catch 가 재시도 가능한 실패를 삭제로 바꾸던 형태를 고쳤다. - **정착하지 않은 핸들러를 대신 ack 하지 않고 requeue 하는 것.** - **요구 재큐 대신 지연 재시도 큐를 쓴 것과, TTL 이 큐 머리에서 평가된다는 한계를 javadoc 에 남긴 것.** - **배수 중 진행 배달을 끝내게 한 것.** - **네이티브 죽은 편지를 검증된 곳에서만 쓰고 나머지는 공유 조율자에 위임한 것, 그리고 네이티브 경로의 증거를 `BROKER_ACK` 로만 주장한 것.** - **자격증명을 연결 시도마다 해석하고 짧은 수명 객체로 넘긴 것, `AmqpCredentials` 를 record 가 아니라 class 로 둔 것과 그 근거.** - **내구 작업 큐에 쿼럼 큐를 요구한 것과 그 근거.** - **배치 누적에 나이 경계를 필수로 만든 것** — 조용한 큐가 마지막 메시지를 미확인으로 붙들지 않게. - **prefetch 가 배치 크기보다 작으면 교착이라는 것을 거부로 표현한 것.** - **배치를 `settlableAsBatch=false` 로 보고한 것** — AMQP multiple-ack 은 진행 중인 작업까지 정착시킨다. --- ## Source anchors ``` src/messaging/messaging-rabbit/build.gradle main/java/…/rabbit/RabbitConfirmCoordinator.java:1-279 (§17.2 returned:74-79) main/java/…/rabbit/RabbitMessagingTransport.java:1-246 (능력 상수 41-43 · §17.4) main/java/…/rabbit/RabbitConsumerRegistrar.java:1-238 (§17.5 pause:694-697) main/java/…/rabbit/RabbitDeliveryMapper.java:1-195 (§12.3 attemptOf:133-153) main/java/…/rabbit/RabbitHeaderMapper.java:1-180 (§17.1 배달 모드 235 · §17.2 순번 헤더 부재) main/java/…/rabbit/RabbitSecurityConfigurer.java:1-179 (§17.3 switch 438-463) main/java/…/rabbit/RabbitBatchConsumerRegistrar.java:1-165 main/java/…/rabbit/RabbitTopologyProfile.java:1-137 main/java/…/rabbit/RabbitDeadLetterPublisher.java:1-130 (§12.2 참조 0) main/java/…/rabbit/RabbitPublishFailureClassifier.java:1-117 main/java/…/rabbit/RabbitSettlementController.java:1-84 main/java/…/rabbit/RabbitProfileValidator.java:1-83 (§17.1 validateDestination:543-555) main/java/…/rabbit/RabbitPublishMapper.java:1-72 main/java/…/rabbit/RabbitNativeDeadLetterCapability.java:1-70 main/java/…/rabbit/RabbitRetryQueueTopology.java:1-57 (§17.4) main/java/…/rabbit/{RabbitSettlementOperations:1-50, RabbitBrokerProfile:1-50, RabbitChannelPublisher:1-42, RabbitPublishReference:1-37, RabbitRequestReply:1-32} test/java/…/rabbit/ 10파일 1,727줄 (RabbitRuntimeTest:309 · RabbitContractHarness:304 · RabbitBrokerIT:232 …) test/java/…/rabbit/RabbitBrokerIT.java:726-733, 786-813 (§17.2 시험이 메우는 x-seq 규약) messaging-spring-boot-starter/…/MessagingProviderSelection.java:64-69 (§12.1 rabbit 거부) messaging-policy/…/DefaultRetryDecisionEngine.java:64 (§17.4 delayedDelivery 소비처) ```