Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f006-messaging-cloudevents.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

12 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn body assets evidence source
CASE a19-f006-messaging-cloudevents messaging-cloudevents는 출하 leaf이고 starter의 의존이며 소비자가 없다 messaging-and-outbox clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a19-f006-messaging-cloudevents 2026-09-04 case-a19-f006-messaging-cloudevents.body.md
key file
a19-f006-messaging-cloudevents ../../../final/evidence/rendered/a19-f006-messaging-cloudevents.svg
../../../final/evidence/raw/a19-f006-messaging-cloudevents.txt
원본 분석 절은 analysis/19-messaging-platform.md#L431 이다.

messaging-cloudevents는 출하 leaf이고 starter의 의존이며 소비자가 없다

modules.json 이 이 리프의 런타임 소속을 app-bootstrap 으로 적고 messaging-spring-boot-starter/build.gradle:26implementation 으로 물어 출하 산출물에 들어간다. 그런데 new DefaultCloudEventMapperCloudEventMappingTest:30 한 줄뿐이고, CloudEventMapperCloudEventExtensions 를 참조하는 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 언급을 각각 센다.

본문

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:26implementation 으로 문다. 선언만이 아니다. 해소된 런타임 프로젝트 클로저 파일에 이 리프 이름이 있고, app-bootstrap/gradle.lockfileio.cloudevents:cloudevents-api:4.0.1cloudevents-core:4.0.1productionRuntimeClasspath 로 등재한다.

implementation 선언 바로 위 :24 의 주석이 이 그룹의 성격을 적는다 — 자동설정이 배선하고 채택자의 소스에서 이름을 대는 일이 없다는 것이다. 이 리프에 대해서는 앞뒤가 다 어긋난다. 배선하는 자동설정이 없고, implementation 이라 채택자의 컴파일 클래스패스에 타입이 오르지 않아 이름을 댈 수도 없다.

부르는 코드가 없다

new DefaultCloudEventMapper 는 저장소 전체에서 CloudEventMappingTest:30 한 줄이다. CloudEventMapperCloudEventExtensions 를 참조하는 main 파일은 이 리프의 셋 자신이다.

파일 이름 규칙에 기대지 않고 CloudEvent 라는 문자열을 main 자바 소스 전체에서 찾아도 나오는 파일은 같은 셋뿐이다. 이름이 *AutoConfiguration.java 인 36 개와 AutoConfiguration.imports 등재 14 개는 그 전체 집합의 부분집합이므로, 자동설정에 없다는 것은 그 계수와 무관하게 성립한다. META-INF/services 파일은 저장소에 하나도 없다.

그래서 CloudEvents 헤더를 기대하는 소비자와 맺어지는 계약이 없다

이 리프가 담은 것은 유선 상호운용 규격이다. 설계 스펙이 :35 의 역량 표에서 CloudEvents 를 도메인·통합 이벤트에 선택 가능한 1.0.2 호환 프로파일로 제공한다고 적고, :756 부터 한 절을 그 프로파일에 쓴다.

그 약속을 받아 실행하는 코드가 어느 경로에도 없다. 위험은 안 쓰이는 코드가 산출물에 들어간다는 것이 아니라, 외부 소비자가 CloudEvents 헤더로 메시지를 받을 것으로 기대할 때 그 기대와 맺어지는 계약이 어느 실행 경로에서도 성립하지 않는다는 것이다.

매핑의 한쪽 방향에는 호출자가 있다

CanonicalEnvelopeHeaders.restatesEnvelopeFieldKafkaHeaderMapper:90·:141RabbitHeaderMapper:102·:158 이 부른다. 다만 이 넷은 값을 싣는 코드가 아니다. :90 은 사용자 헤더가 봉투 필드를 사칭하면 RESERVED_HEADER_FORGED 로 던지고, :141 은 되읽을 때 건너뛴다. 실제로 헤더를 싣는 것은 KafkaHeaderMapper:36 부터의 put(headers, ReservedHeaders.MESSAGE_ID, …) 계열이다.

그 넷 중 조립되는 경로에 있는 것도 하나다. KafkaPublishMapper:34KafkaHeaderMapper 를 만들고, 그 위의 KafkaMessagingTransportKafkaMessagingAutoConfiguration:168 이 만든다. RabbitMessagingTransport 를 만드는 main 코드는 0 건이라 Rabbit 쪽 두 줄은 조립되지 않은 클래스 안에 있다.

정리하면 정규 봉투 헤더 판정은 Kafka 발행 경로에서 실제로 지나가고, 그 헤더를 CloudEventExtensions 로 옮기는 코드는 DefaultCloudEventMapper 안에만 있으며 그것을 부르는 main 코드가 없다.

원문과 갈리는 자리

원문은 자동설정을 28 개 클래스로 적었다. 28 은 스타터의 autoconfigure 패키지 파일 수다. 그중 이름이 *AutoConfiguration 인 것은 여섯이고 나머지는 설정 값 타입과 검증기와 발행자다. 근거가 없는 수가 아니라 그 28 개를 "자동설정 클래스" 라고 부른 이름이 부정확하다.

원문은 CanonicalEnvelopeHeadersCloudEventExtensions 사이에 매핑이 존재한다고 적었다. 두 클래스는 서로를 참조하지 않는다. DefaultCloudEventMapper 가 양쪽 개념을 각각 다루는 것이지 두 타입이 이어져 있지는 않다.

판정과 등급은 원문과 같다.

확인하지 못한 것

헤더를 실은 메시지를 실제로 흘려보내 보지는 않았다. 부르는 경로가 없다는 데서 멈췄다.

리플렉션이나 서비스 로더로 이 매퍼를 가져가는 경로는 META-INF/services 파일이 0 개라는 것까지만 확인했고, 클래스 이름 문자열로 불러 쓰는 경로는 따로 세지 않았다. 판정 근거는 이름 기반 정적 검색이다.

리플렉션이나 서비스 로더로 이 매퍼를 가져가는 경로는 META-INF/services 파일이 0 개라는 것까지만 확인했고, 클래스 이름 문자열로 불러 쓰는 경로는 따로 세지 않았다. 판정 근거는 이름 기반 정적 검색이다.

지원 매트릭스와 설정 참조 문서에 CloudEvents 언급이 0 건이라, 실제 외부 소비자가 이 약속을 보고 붙었는지는 확인할 방법이 없었다.