--- kind: CASE slug: the-transport-and-the-validator-answer-differently title: 같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 record 의 의미와 어긋난다 topic: capability-declaration-vs-proof project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:the-transport-and-the-validator-answer-differently evidenceCapturedOn: 2026-09-01 assets: - key: the-transport-and-the-validator-answer-differently file: ../../../final/evidence/rendered/the-transport-and-the-validator-answer-differently.svg evidence: - ../../../final/evidence/raw/the-transport-and-the-validator-answer-differently.txt source: - 원본 분석 절은 analysis/messaging/messaging-pulsar-experimental.md §17.1 이다. --- # 같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 record 의 의미와 어긋난다 Pulsar 어댑터에서 키 공유 구독의 능력을 전송과 검증기가 다르게 답한다. 성분 문서를 기준으로 보면 검증기 쪽이 맞고 전송 쪽이 자기 안에서 모순인데, 런타임이 읽는 것은 전송 쪽이다. ## 관계 - **능력 선언의 세 출처와 그것이 파생되지 않을 때** 이 사례가 속한 구조다. - **능력 플래그의 무게는 그것을 읽는 코드가 정한다** 어느 쪽이 틀렸는지가 아니라 어느 쪽이 읽히는지가 심각도를 정한다. - **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** 같은 판정 절차의 일반형이다. ## 문제 이 어댑터는 두 구독 종류를 노출한다. 공유 구독은 경쟁 소비자에 순서 없음이고, 키 공유 구독은 경쟁 소비자에 키별 순서다. 능력을 답하는 자리가 둘이다. 전송이 구독 종류에 따라 두 상수 중 하나를 고르고, 검증기가 같은 판단을 자기 메서드로 한다. ## 결론 키 공유에 대해 두 답이 갈린다. 전송은 순서 있는 스트림을 거짓, 키별 순서를 참으로 답한다. 검증기는 둘 다 참으로 답한다. 성분 문서가 판정 기준이다. 순서 있는 스트림은 순서 단위 안에서 순서가 보존되는지를 뜻하고, 키 공유의 순서 단위는 키다. 그 단위 안에서 순서는 보존된다. 그러므로 검증기 쪽이 문서화된 의미와 맞다. 전송 쪽은 자기 안에서도 모순이다. 키별 순서를 참이라고 하면서 순서 있는 스트림을 거짓이라고 하면, 순서가 보존되는 단위가 있는데 그 단위 안에서 순서가 보존되지 않는다는 말이 된다. 그리고 어긋난 쪽이 런타임이 읽는 쪽이다. 목적지별 능력을 돌려주는 것은 SPI 메서드이고 그것을 구현하는 것은 전송이다. 순서 있는 스트림은 이 저장소에서 production 코드가 실제로 읽는 몇 안 되는 능력 중 하나로, 재시도 결정 엔진이 그 값을 보고 순서 보존 재시도를 고를지 정한다. 결과적으로 키별 순서를 약속한 목적지가 순서 보존 재시도를 받지 못한다. 두 리터럴을 묶는 것은 아무것도 없다. 열두 개의 불리언이 두 파일에 각각 손으로 적혀 있다. 테스트는 키별 순서만 단언하고 순서 있는 스트림은 보지 않는다. 자매 어댑터인 NATS 는 두 곳이 같은 값을 답한다. 다만 그 일치도 공유가 아니라 손으로 복사한 리터럴이므로, 오늘 같다는 것이 내일도 같으리라는 보장은 코드에 없다. ## 검증 환경 OpenJDK : 21.0.12 Gradle : 9.0.0 확인 방식 : 두 열두 성분 리터럴의 성분별 대조와 성분 문서 확인 소스 수정 : x ## 재현 조건 1. 전송의 키 공유용 능력 상수 열두 성분을 순서대로 적는다. 2. 검증기의 능력 메서드가 키 공유에 대해 만드는 열두 성분을 적는다. 3. 두 목록을 성분별로 대조한다. 4. 능력 record 의 성분 문서에서 두 이름의 정의를 읽는다. 5. 순서 있는 스트림을 읽는 production 코드를 찾는다. ## 본문 Key_Shared 구독에 대해 전송은 `orderedStream=false, keyedOrdering=true` 를, 검증기는 `orderedStream=true, keyedOrdering=true` 를 답한다. ## 성분 문서가 판정 기준이다 `orderedStream` 은 "순서 단위 안에서 순서가 보존되는가" 이고 Key_Shared 의 순서 단위는 키다. 그러므로 검증기 쪽이 문서화된 의미와 맞고, 전송 쪽은 자기 안에서 모순이다. ## 어긋난 쪽이 런타임이 읽는 쪽이다 :::evidence key="the-transport-and-the-validator-answer-differently" alt="코드베이스에서 DefaultRetryDecisionEngine 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultRetryDecisionEngine 코드베이스 검색 — 3줄 · exit 0" zoom="true" ::: `capabilities(DestinationName)` 이 SPI 메서드이고 `orderedStream` 은 production 코드가 실제로 읽는 세 능력 중 하나다 — `DefaultRetryDecisionEngine` 이 그 값으로 순서 보존 재시도를 고른다. ## 두 리터럴을 묶는 것이 없다 테스트는 `keyedOrdering` 만 단언해 `orderedStream` 을 보지 않는다. 자매 어댑터 NATS 는 두 곳이 같은 값을 답하지만 그 일치도 공유가 아니라 손으로 복사한 리터럴이다. ## 확인하지 못한 것 두 답이 실제 재시도 선택을 어떻게 가르는지 실행으로 재현하지 않았다. 이 가족은 배선 경로가 없다.