- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다 - 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5 (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를 techviz 로 만들었다 - 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs 돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다. Concept 이 인용한 코드가 SSOT 에 없어 뺐다 - candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
128 lines
12 KiB
Markdown
128 lines
12 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a19-f006-messaging-cloudevents
|
|
title: messaging-cloudevents는 출하 leaf이고 starter의 의존이며 소비자가 없다
|
|
topic: messaging-and-outbox
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:a19-f006-messaging-cloudevents
|
|
evidenceCapturedOn: 2026-09-04
|
|
body: case-a19-f006-messaging-cloudevents.body.md
|
|
assets:
|
|
- key: a19-f006-messaging-cloudevents
|
|
file: ../../../final/evidence/rendered/a19-f006-messaging-cloudevents.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a19-f006-messaging-cloudevents.txt
|
|
source:
|
|
- 원본 분석 절은 final/document.md#a19#L431 이다.
|
|
---
|
|
|
|
# messaging-cloudevents는 출하 leaf이고 starter의 의존이며 소비자가 없다
|
|
|
|
`modules.json` 이 이 리프의 런타임 소속을 `app-bootstrap` 으로 적고 `messaging-spring-boot-starter/build.gradle:26` 이 `implementation` 으로 물어 출하 산출물에 들어간다. 그런데 `new DefaultCloudEventMapper` 는 `CloudEventMappingTest:30` 한 줄뿐이고, `CloudEventMapper` 와 `CloudEventExtensions` 를 참조하는 main 파일은 이 리프의 세 파일 자신이다. main 자바 소스 전체에서 `CloudEvent` 를 언급하는 파일도 그 셋뿐이다.
|
|
|
|
## 관계
|
|
|
|
- **outbox 가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다**
|
|
저쪽은 조건이 참이 될 수 없어 사슬 전체가 조립되지 않고, 여기는 조립하는 코드가 아예 없다. 둘 다 main 파일은 남아 있는데 그것을 실행하는 경로가 없다.
|
|
- **브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다**
|
|
두 기록 모두 출하 리프의 타입이 main 코드에서 한 번도 참조되지 않는다. 저쪽은 자바독이 약속한 시작 시 대조가 실행되지 않고, 여기는 CloudEvents 헤더 매핑을 부르는 경로가 없다.
|
|
- **runtime_memberships를 먼저 읽고 심각도를 정한다**
|
|
이 리프의 런타임 소속이 `app-bootstrap` 이라 빌드 전용 리프에 주는 미조립 면제가 적용되지 않고, 그래서 세 main 파일의 미조립이 기록할 사건이 된다.
|
|
|
|
## 문제
|
|
|
|
이 리프는 CloudEvents 상호운용 규격을 담는다.
|
|
|
|
런타임 소속이 app-bootstrap 이라 세 main 파일이 출하 산출물에 들어가므로, 그 세 파일을 부르는 main 코드가 있는지 셌다.
|
|
|
|
## 결론
|
|
|
|
없다.
|
|
|
|
DefaultCloudEventMapper(171줄)를 생성하는 코드는 CloudEventMappingTest:30 하나다. 계약 타입 둘도 리프 밖 main 에서는 나오지 않고, 파일 이름 규칙 없이 CloudEvent 를 main 전수 검색해도 결과가 같다. 서비스 로더가 읽을 등록 파일도 저장소에 없다.
|
|
|
|
출하는 선언만이 아니라 해소된 결과로도 확인된다. 런타임 프로젝트 클로저에 이 리프가 있고 app-bootstrap/gradle.lockfile 이 io.cloudevents 두 아티팩트를 productionRuntimeClasspath 로 등재한다.
|
|
|
|
빌드 파일이 스스로 모순을 적어 둔 자리도 있다. messaging-spring-boot-starter/build.gradle:24 의 주석은 이 그룹을 자동설정이 배선하고 채택자가 이름을 대지 않는 것으로 적는데, 이 리프는 배선하는 자동설정이 없고 implementation 이라 채택자가 이름을 댈 수도 없다.
|
|
|
|
위험은 안 쓰이는 코드가 있다는 것이 아니다. 설계 스펙은 이 프로파일을 제공한다고 적고 한 절을 그 매핑에 할애했는데, 실행으로 옮기는 코드가 어느 경로에도 없다.
|
|
|
|
한 방향에는 부르는 코드가 있다. 봉투 필드 사칭을 판정하는 메서드를 두 브로커 헤더 매퍼가 쓰는데, 그중 조립되는 것은 Kafka 발행 쪽뿐이다. 반대 방향을 맡는 코드는 이 리프 안에 갇혀 있다.
|
|
|
|
판정은 P2 이고 원문과 같다. 다만 지원 매트릭스와 설정 참조 문서에는 CloudEvents 언급이 0 건이라, 약속이 적힌 곳은 설계 스펙까지다.
|
|
|
|
## 검증 환경
|
|
|
|
확인 방식 : 리프 파일과 줄 수 확인, 런타임 소속과 빌드 선언과 해소된 클로저와 잠금 파일 대조, new DefaultCloudEventMapper 와 두 계약 타입의 main 참조 전수, CloudEvent 문자열의 main 전수 검색과 설정 클래스 계수, META-INF/services 계수, 원문이 적은 28 의 출처 대조, CanonicalEnvelopeHeaders 호출 네 줄의 성격과 그 위 transport 의 생성 지점 확인
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 이 리프의 소스 파일을 소스 세트별로 나열하고 줄 수를 센다.
|
|
2. modules.json 의 런타임 소속과 스타터 빌드 선언, 그리고 그 선언 위의 주석을 읽는다.
|
|
3. 해소된 런타임 프로젝트 클로저와 gradle.lockfile 에서 이 리프와 서드파티 아티팩트를 찾는다.
|
|
4. new DefaultCloudEventMapper 를 저장소 전체에서 센다.
|
|
5. 두 계약 타입을 참조하는 main 파일을 나열한다.
|
|
6. CloudEvent 를 main 자바 전수로 검색해 파일 이름 규칙에 기대지 않은 결과를 얻고, 설정 클래스와 서비스 로더 등록 파일도 함께 센다.
|
|
7. 원문이 28 로 적은 수가 어느 디렉터리의 파일 수인지 확인하고 그중 *AutoConfiguration 을 센다.
|
|
8. CanonicalEnvelopeHeaders 를 부르는 main 네 줄이 값을 싣는 코드인지 판정 코드인지 읽고, 실제로 헤더를 싣는 줄을 따로 찾는다.
|
|
9. 그 네 줄 위의 transport 두 개를 만드는 main 코드를 각각 센다.
|
|
10. 설계 스펙과 운영자용 문서에서 CloudEvents 언급을 각각 센다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
`messaging-cloudevents` 는 파일 넷짜리 리프다. main 셋이 `DefaultCloudEventMapper`(171줄), `CloudEventMapper`(33줄), `CloudEventExtensions`(24줄)이고, test 하나가 `CloudEventMappingTest`(162줄)다.
|
|
|
|
## 출하 산출물에 들어간다
|
|
|
|
:::evidence key="a19-f006-messaging-cloudevents" alt="저장소 루트에서 돌린 정적 검색 출력 75줄. 리프의 파일 넷이 소스 세트와 줄 수로 먼저 나오고, 출하를 말하는 네 근거가 이어진다 — modules.json 의 런타임 소속, 스타터 build.gradle 의 주석과 implementation 선언, 해소된 런타임 프로젝트 클로저에 이 리프 이름이 있다는 것, 그리고 gradle.lockfile 에 io.cloudevents 두 아티팩트가 productionRuntimeClasspath 로 등재된 줄이다. 그다음 new DefaultCloudEventMapper 가 자기 시험 한 줄뿐이라는 것과 계약 타입을 참조하는 main 파일이 리프의 셋이라는 것, main 자바 소스 전체에서 CloudEvent 를 언급하는 파일도 그 셋뿐이라는 것, 설정 클래스 계수와 META-INF/services 파일 수 0 이 나온다. 원문이 28 로 적은 수가 스타터 autoconfigure 패키지의 파일 수이고 그중 이름이 AutoConfiguration 인 것은 여섯이라는 대조가 이어진다. 끝으로 CanonicalEnvelopeHeaders 를 부르는 네 줄이 값을 싣는 곳이 아니라 거절과 건너뛰기 판정이라는 것과 실제로 헤더를 싣는 ReservedHeaders 줄, 그리고 두 매퍼 위의 transport 중 Kafka 쪽만 main 에서 만들어진다는 것, 설계 스펙이 CloudEvents 프로파일을 약속한 두 줄과 지원 매트릭스·설정 참조에는 언급이 0 건이라는 것이 보인다." caption="리프 파일 넷 · 출하 근거 넷 · 생성 지점은 시험 하나 · CloudEvent 를 아는 main 파일 셋 · 28 의 출처 · 반대편 네 줄의 성격과 배선 · 약속한 문서와 없는 문서 — 75줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
`modules.json` 이 이 리프의 `runtime_memberships` 를 `["app-bootstrap"]` 으로 적고, `messaging-spring-boot-starter/build.gradle:26` 이 `implementation` 으로 문다. 선언만이 아니다. 해소된 런타임 프로젝트 클로저 파일에 이 리프 이름이 있고, `app-bootstrap/gradle.lockfile` 이 `io.cloudevents:cloudevents-api:4.0.1` 과 `cloudevents-core:4.0.1` 을 `productionRuntimeClasspath` 로 등재한다.
|
|
|
|
그 `implementation` 선언 바로 위 `:24` 의 주석이 이 그룹의 성격을 적는다 — 자동설정이 배선하고 채택자의 소스에서 이름을 대는 일이 없다는 것이다. 이 리프에 대해서는 앞뒤가 다 어긋난다. 배선하는 자동설정이 없고, `implementation` 이라 채택자의 컴파일 클래스패스에 타입이 오르지 않아 이름을 댈 수도 없다.
|
|
|
|
## 부르는 코드가 없다
|
|
|
|
`new DefaultCloudEventMapper` 는 저장소 전체에서 `CloudEventMappingTest:30` 한 줄이다. `CloudEventMapper` 와 `CloudEventExtensions` 를 참조하는 main 파일은 이 리프의 셋 자신이다.
|
|
|
|
파일 이름 규칙에 기대지 않고 `CloudEvent` 라는 문자열을 main 자바 소스 전체에서 찾아도 나오는 파일은 같은 셋뿐이다. 이름이 `*AutoConfiguration.java` 인 36 개와 `AutoConfiguration.imports` 등재 14 개는 그 전체 집합의 부분집합이므로, 자동설정에 없다는 것은 그 계수와 무관하게 성립한다. `META-INF/services` 파일은 저장소에 하나도 없다.
|
|
|
|
## 그래서 CloudEvents 헤더를 기대하는 소비자와 맺어지는 계약이 없다
|
|
|
|
이 리프가 담은 것은 유선 상호운용 규격이다. 설계 스펙이 `:35` 의 역량 표에서 CloudEvents 를 도메인·통합 이벤트에 선택 가능한 1.0.2 호환 프로파일로 제공한다고 적고, `:756` 부터 한 절을 그 프로파일에 쓴다.
|
|
|
|
그 약속을 받아 실행하는 코드가 어느 경로에도 없다. 위험은 안 쓰이는 코드가 산출물에 들어간다는 것이 아니라, 외부 소비자가 CloudEvents 헤더로 메시지를 받을 것으로 기대할 때 그 기대와 맺어지는 계약이 어느 실행 경로에서도 성립하지 않는다는 것이다.
|
|
|
|
## 매핑의 한쪽 방향에는 호출자가 있다
|
|
|
|
`CanonicalEnvelopeHeaders.restatesEnvelopeField` 를 `KafkaHeaderMapper:90`·`:141` 과 `RabbitHeaderMapper:102`·`:158` 이 부른다. 다만 이 넷은 값을 싣는 코드가 아니다. `:90` 은 사용자 헤더가 봉투 필드를 사칭하면 `RESERVED_HEADER_FORGED` 로 던지고, `:141` 은 되읽을 때 건너뛴다. 실제로 헤더를 싣는 것은 `KafkaHeaderMapper:36` 부터의 `put(headers, ReservedHeaders.MESSAGE_ID, …)` 계열이다.
|
|
|
|
그 넷 중 조립되는 경로에 있는 것도 하나다. `KafkaPublishMapper:34` 가 `KafkaHeaderMapper` 를 만들고, 그 위의 `KafkaMessagingTransport` 를 `KafkaMessagingAutoConfiguration:168` 이 만든다. `RabbitMessagingTransport` 를 만드는 main 코드는 0 건이라 Rabbit 쪽 두 줄은 조립되지 않은 클래스 안에 있다.
|
|
|
|
정리하면 정규 봉투 헤더 판정은 Kafka 발행 경로에서 실제로 지나가고, 그 헤더를 `CloudEventExtensions` 로 옮기는 코드는 `DefaultCloudEventMapper` 안에만 있으며 그것을 부르는 main 코드가 없다.
|
|
|
|
## 원문과 갈리는 자리
|
|
|
|
원문은 자동설정을 28 개 클래스로 적었다. 28 은 스타터의 `autoconfigure` 패키지 파일 수다. 그중 이름이 `*AutoConfiguration` 인 것은 여섯이고 나머지는 설정 값 타입과 검증기와 발행자다. 근거가 없는 수가 아니라 그 28 개를 "자동설정 클래스" 라고 부른 이름이 부정확하다.
|
|
|
|
원문은 `CanonicalEnvelopeHeaders` 와 `CloudEventExtensions` 사이에 매핑이 존재한다고 적었다. 두 클래스는 서로를 참조하지 않는다. `DefaultCloudEventMapper` 가 양쪽 개념을 각각 다루는 것이지 두 타입이 이어져 있지는 않다.
|
|
|
|
판정과 등급은 원문과 같다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
헤더를 실은 메시지를 실제로 흘려보내 보지는 않았다. 부르는 경로가 없다는 데서 멈췄다.
|
|
|
|
리플렉션이나 서비스 로더로 이 매퍼를 가져가는 경로는 `META-INF/services` 파일이 0 개라는 것까지만 확인했고, 클래스 이름 문자열로 불러 쓰는 경로는 따로 세지 않았다. 판정 근거는 이름 기반 정적 검색이다.
|
|
|
|
리플렉션이나 서비스 로더로 이 매퍼를 가져가는 경로는 `META-INF/services` 파일이 0 개라는 것까지만 확인했고, 클래스 이름 문자열로 불러 쓰는 경로는 따로 세지 않았다. 판정 근거는 이름 기반 정적 검색이다.
|
|
|
|
지원 매트릭스와 설정 참조 문서에 CloudEvents 언급이 0 건이라, 실제 외부 소비자가 이 약속을 보고 붙었는지는 확인할 방법이 없었다.
|
|
|
|
<!-- body:end -->
|