--- kind: CONCEPT slug: messaging-runtime-core-c03 title: 바이트가 프로세스를 떠났는가가 REJECTED와 AMBIGUOUS를 가른다 topic: delivery-and-settlement-models project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: concept:messaging-runtime-core-c03 evidenceCapturedOn: 2026-09-01 assets: - key: messaging-runtime-core-c03 file: ../../../final/evidence/rendered/messaging-runtime-core-c03.svg - key: messaging-runtime-core-c03-diagram file: ../../../final/assets/diagrams/messaging-runtime-core-c03.svg evidence: - ../../../final/evidence/raw/messaging-runtime-core-c03.txt source: - 원본 분석 절은 analysis/messaging/messaging-runtime-core.md#L128 이다. module: messaging-runtime-core --- # 바이트가 프로세스를 떠났는가가 REJECTED와 AMBIGUOUS를 가른다 실제 순서 여덟 단계: 1–7은 전부 REJECTED, 8만 AMBIGUOUS다. 그 경계가 정확히 "바이트가 프로세스를 떠났는가"다. ## 관계 - **만들어 두고 흘리지 않는 진단값은 진단이 아니다** 같은 분석 리프에서 끌어낸 규칙이다. - **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** 같은 분석 리프에서 끌어낸 규칙이다. - **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** 같은 분석 리프에서 끌어낸 규칙이다. - **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** 같은 분석 리프에서 끌어낸 규칙이다. ## 본문 순서가 인터셉터 map에서 조립되지 않고 고정돼 있으며, javadoc이 각 단계의 위치를 결정으로 적는다 — 목적지와 접근이 먼저라 인가되지 않은 발행이 payload를 인코딩하지 않고, 인코딩이 admission보다 먼저인 것은 admission 경계가 바이트에 걸려 있어 인코딩 전에는 바이트 수를 모르기 때문이며, 런타임 lease가 전송 직전 마지막인 것은 이미 in-flight 한도에 계상된 메시지 밑에서 rotation이 transport를 바꾸지 못하게 하기 위해서다. ## 발행 단계가 고정된 순서 :::evidence key="messaging-runtime-core-c03-diagram" alt="목적지와 접근에서 인코딩으로 이름 확인이 건너가고 인코딩에서 admission 으로 바이트 수가 건너가고 admission 에서 브로커 전송으로 permit 이 건너간다" caption="발행 단계가 고정된 순서" zoom="false" ::: ## 여덟 단계 중 여덟만 AMBIGUOUS다 **1–7은 전부 `REJECTED`, 8만 `AMBIGUOUS`다.** 그 경계가 정확히 "바이트가 프로세스를 떠났는가"다. `messaging-core-api`의 3상태(§4.1)가 여기서 실제 분기가 된다. 그리고 `rejected(...)`가 만드는 `PublishResult`는 `PublishEvidence.notTransmitted()`를 쓰므로 `PublishResult` 생성자의 14가지 금지 조합 검증을 자연히 통과한다. ## PublishResult 참조 위치 :::evidence key="messaging-runtime-core-c03" alt="코드베이스에서 PublishResult 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PublishResult 코드베이스 검색 — 25줄 · exit 0" zoom="true" ::: ## 이미 기다리기를 그만둔 메시지를 보내지 않는다 `remainingBudget`이 `timeout - elapsedSince(startedAt)`이고, 0 이하면 전송 전에 `REJECTED`로 끝낸다 — "Sending anyway would start a message the caller has already stopped waiting for." ## copy 에 타임아웃을 거는 이유 `orTimeout`을 원본에 걸면 만료가 어댑터의 stage를 완료시켜 어댑터의 자기 정리가 깨진다. 복사본에 걸면 만료는 이쪽 경로만 끝내고 어댑터는 자기 in-flight를 계속 소유한다. 그 대가도 명시돼 있다 — permit과 lease는 **복사본이 완료될 때** 반납되므로, 브로커가 나중에 응답해도 이미 반납된 상태다. 그것이 의도다("holding them until a stalled broker answers is how a rotation waits forever"). ## 두 경우를 한 블록에서 처리한다 `handle`은 `whenComplete`와 달리 실패를 삼키고 값을 반환한다. `lease.close()`는 `MessagingRuntimeLease` 계약상 멱등이고(`transport-spi` §4.1), `admission.complete`도 미보유 목적지에 대해 무해하다(`messaging-policy` §4.3). ## 한 가지 비대칭 6번(`admit`)이 예외를 던지면 그 예외가 그대로 호출자에게 전파된다 — `try` 블록 밖이다. 다른 모든 실패는 `PublishResult`로 정규화되는데 admission 실패만 예외다.