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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+108
@@ -0,0 +1,108 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: capability-constant-outlives-its-condition
|
||||
title: 능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:capability-constant-outlives-its-condition
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-capability-constant-outlives-its-condition.body.md
|
||||
assets:
|
||||
- key: capability-constant-outlives-its-condition
|
||||
file: ../../../final/evidence/rendered/capability-constant-outlives-its-condition.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/capability-constant-outlives-its-condition.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-nats-experimental.md#L111 이다.
|
||||
---
|
||||
|
||||
# 능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다
|
||||
|
||||
어댑터가 중복 제거 지원을 상수로 참이라 답한다. 실제 중복 제거 식별자는 프로파일에 창이 있을 때만 실린다. 창은 선택 사항이고 아무도 요구하지 않는다. 그리고 이 플래그는 부재가 예외를 만드는 유일한 능력이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다**
|
||||
같은 통독에서 나온 짝이다.
|
||||
- **지원 매트릭스가 코드와 반대를 말한다**
|
||||
같은 능력 표의 반대 방향 사례다.
|
||||
- **조용히 약해진 보증은 사고 전까지 동작하는 것과 구분되지 않는다**
|
||||
능력 레코드의 자바독이 적은 근거다.
|
||||
|
||||
## 문제
|
||||
|
||||
능력 레코드의 자바독이 계약을 선언한다.
|
||||
|
||||
프로파일이 여기 없는 것을 요구하면 플랫폼이 크게 실패한다는 것이다. 조용히 저하되지 않는다는 것이고, 그 이유는 조용히 약해진 보증이 사고가 나기 전까지 동작하는 보증과 구분되지 않기 때문이라는 것이다.
|
||||
|
||||
이 어댑터가 그 계약을 어떻게 답하는지 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
상수로 답한다.
|
||||
|
||||
능력 값이 정적 최종 필드이고 프로파일을 보지 않는다. 그중 중복 제거 발행이 참이다.
|
||||
|
||||
그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다. 창이 비어 있으면 식별자가 없고, 헤더가 실리지 않고, 서버는 중복을 제거하지 않는다.
|
||||
|
||||
창은 선택 사항이다. 프로파일 생성자도 시작 검증기도 창을 요구하지 않는다.
|
||||
|
||||
이 플래그가 특별한 이유가 있다.
|
||||
|
||||
능력 열둘 중 부재가 예외를 만드는 유일한 것이다. 나머지는 읽히지 않거나 분기에만 쓰인다.
|
||||
|
||||
그러므로 창 없는 목적지가 그 가드를 통과한다. 가드는 어댑터가 참이라 답했으니 통과시킨다.
|
||||
|
||||
그리고 어댑터 자신이 그 조건을 알고 있다.
|
||||
|
||||
클래스 자바독이 시간 초과 발행을 모호로 다루는 이유를 적으면서, 그 모호를 재시도해도 안전하게 만드는 것은 중복 제거 창이며 프로파일이 그것을 켰을 때라고 적는다.
|
||||
|
||||
프로파일이 그것을 켰을 때라는 조건이 정확히 능력이 담지 않은 것이다.
|
||||
|
||||
창이 없는 목적지에서 모호를 재시도하면 스트림에 같은 메시지가 두 번 들어간다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
확인 방식 : 능력 상수와 식별자 생성 경로 대조, 프로파일 생성자와 검증기의 요구 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 document-detail 의 analysis/messaging/messaging-nats-experimental.md 에 있다.
|
||||
|
||||
1. 어댑터의 능력 상수를 읽고 각 성분의 의미를 레코드 문서에서 확인한다.
|
||||
2. 중복 제거 식별자를 만드는 메서드를 읽는다.
|
||||
3. 프로파일의 창 필드가 선택 사항인지 확인한다.
|
||||
4. 생성자와 검증기가 창을 요구하는지 확인한다.
|
||||
5. 그 플래그의 부재가 어디서 예외를 만드는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
어댑터가 `deduplicatedPublish=true` 를 상수로 답한다. 그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어지고, 창은 선택 사항이며 생성자도 검증기도 요구하지 않는다.
|
||||
|
||||
## 중복 제거 식별자가 만들어지는 조건
|
||||
|
||||
:::evidence key="capability-constant-outlives-its-condition" alt="분석 문서 analysis/messaging/messaging-nats-experimental.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-nats-experimental.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 창이 없으면 서버가 중복을 제거하지 않는다
|
||||
|
||||
`Nats-Msg-Id` 가 실리지 않기 때문이다.
|
||||
|
||||
## 하필 그 플래그다
|
||||
|
||||
이 플래그는 능력 열둘 중 부재가 예외를 만드는 유일한 것이라, 창 없는 목적지가 그 가드를 통과한 뒤 모호 재발행에서 스트림에 같은 메시지를 두 번 넣는다.
|
||||
|
||||
## 어댑터 자신의 javadoc 이 조건을 안다
|
||||
|
||||
재시도를 안전하게 만드는 것은 창이며 "프로파일이 그것을 켰을 때" 라고 적는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
창 없는 프로파일로 모호 재발행을 실행해 중복 저장을 재현하지 않았다. 이 리프는 실험 등급이고 배선 경로가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-spring-boot-starter-f02
|
||||
title: 자동 설정이 transport 를 읽지 않고 전송을 하드코딩한다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-spring-boot-starter-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-spring-boot-starter-f02
|
||||
file: ../../../final/evidence/rendered/grpc-spring-boot-starter-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-spring-boot-starter-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-spring-boot-starter.md#L221 이다.
|
||||
module: grpc-spring-boot-starter
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 자동 설정이 transport 를 읽지 않고 전송을 하드코딩한다
|
||||
|
||||
GrpcPlatformProperties.transport 는 GrpcServerTransport 열거형이고 기본값이 NETTY_SHADED 다. 그 값을 자동 설정이 보지 않으므로 다른 값을 설정해도 만들어지는 프로파일은 같다.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcPlatformProperties.transport 는 GrpcServerTransport 열거형이고 기본값이 NETTY_SHADED 다.
|
||||
|
||||
그 값을 자동 설정이 보지 않으므로 다른 값을 설정해도 만들어지는 프로파일은 같다.
|
||||
|
||||
## 결론
|
||||
|
||||
지금은 무해에 가깝다 — 기본값이 하드코딩된 것과 같고, 다른 값은 §17.1 때문에 거부되지도 않지만 반영되지도 않는다.
|
||||
|
||||
그러나 설정 키가 존재하고 문서화되어 있으므로 운영자는 그것이 전송을 고른다고 읽는다.
|
||||
|
||||
수정은 프로파일 팩토리를 transport 로 분기시키거나, 그 키를 검증 전용임을 자바독에 명시하는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcPlatformProperties 참조 19건 검색과 자동 설정이 transport 값을 읽는지 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-spring-boot-starter.md#L221 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcPlatformProperties.transport` 는 `GrpcServerTransport` 열거형이고 기본값이 `NETTY_SHADED` 다. 그 값을 자동 설정이 보지 않으므로 다른 값을 설정해도 만들어지는 프로파일은 같다.
|
||||
|
||||
## GrpcPlatformProperties 참조 위치
|
||||
|
||||
:::evidence key="grpc-spring-boot-starter-f02" alt="코드베이스에서 GrpcPlatformProperties 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcPlatformProperties 코드베이스 검색 — 19줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 지금은 무해에 가깝다
|
||||
|
||||
기본값이 하드코딩된 것과 같고, 다른 값은 §17.1 때문에 거부되지도 않지만 반영되지도 않는다.
|
||||
|
||||
## 그래도 운영자는 그것이 전송을 고른다고 읽는다
|
||||
|
||||
설정 키가 존재하고 문서화되어 있다. 수정은 프로파일 팩토리를 `transport` 로 분기시키거나, 그 키를 검증 전용임을 자바독에 명시하는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
다른 transport 값을 설정해 만들어지는 프로파일이 같은지 실행으로 확인하지 않았다. 자동 설정이 그 값을 읽지 않는다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-runtime-f02
|
||||
title: 토폴로지 검증 스택이 두 벌이고 판정이 어긋난다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-runtime-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-runtime-f02
|
||||
file: ../../../final/evidence/rendered/messaging-admin-runtime-f02.svg
|
||||
- key: messaging-admin-runtime-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-admin-runtime-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-runtime-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-runtime.md#L910 이다.
|
||||
module: messaging-admin-runtime
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 토폴로지 검증 스택이 두 벌이고 판정이 어긋난다
|
||||
|
||||
Stack A 는 파티션 스케일업을 ADVISORY 로 두어 기동을 허용하고 그 근거를 명시한다. Stack B 는 같은 상황을 차이로 보고 기동을 거부한다.
|
||||
|
||||
## 문제
|
||||
|
||||
Stack A 는 파티션 스케일업을 ADVISORY 로 두어 기동을 허용하고 그 근거를 명시한다.
|
||||
|
||||
Stack B 는 같은 상황을 차이로 보고 기동을 거부한다.
|
||||
|
||||
## 결론
|
||||
|
||||
둘 다 프로덕션 호출부가 0건이라 지금은 충돌하지 않지만, analysis/messaging/messaging-admin-api.md §17 첫 항목대로 토폴로지 검증을 기동에 배선하는 순간 어느 스택을 배선하느냐가 스케일업한 배포의 기동 여부를 가른다.
|
||||
|
||||
Stack A 가 남아야 할 것으로 보인다 — severity 구분, physicalName 검사, 근거 주석이 있고 테스트도 13건으로 더 두껍다.
|
||||
|
||||
Stack B(TopologyValidationRuntime, TopologyReader, ObservedTopology, 그리고 그것만 쓰는 TopologyManifest.differencesFrom)를 제거하는 편이 낫다.
|
||||
|
||||
같은 코드 문자열 TOPOLOGY_MISMATCH 를 두 예외 타입이 쓰는 것도 정리 대상이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : TopologyValidationRuntime 참조 13건 검색과 두 스택이 같은 상황에 내리는 판정 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-runtime.md#L910 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
Stack A 는 파티션 스케일업을 ADVISORY 로 두어 기동을 허용하고 그 근거를 명시한다. Stack B 는 같은 상황을 차이로 보고 기동을 거부한다.
|
||||
|
||||
## 두 스택이 갈리는 지점
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f02-diagram" alt="스택 A 쪽에 파티션 스케일업과 ADVISORY 기동 허용이 놓이고 스택 B 쪽에 파티션 스케일업과 차이 기동 거부가 놓인다" caption="두 스택이 갈리는 지점" zoom="false"
|
||||
:::
|
||||
|
||||
## TopologyValidationRuntime 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f02" alt="코드베이스에서 TopologyValidationRuntime 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TopologyValidationRuntime 코드베이스 검색 — 13줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 배선하는 순간 기동 여부가 갈린다
|
||||
|
||||
둘 다 프로덕션 호출부가 0건이라 지금은 충돌하지 않지만, `analysis/messaging/messaging-admin-api.md` §17 첫 항목대로 토폴로지 검증을 기동에 배선하는 순간 **어느 스택을 배선하느냐가 스케일업한 배포의 기동 여부를 가른다**.
|
||||
|
||||
## 남아야 할 쪽
|
||||
|
||||
Stack A 로 보인다 — severity 구분, `physicalName` 검사, 근거 주석이 있고 테스트도 13건으로 더 두껍다. Stack B(`TopologyValidationRuntime`, `TopologyReader`, `ObservedTopology`, 그리고 그것만 쓰는 `TopologyManifest.differencesFrom`)를 제거하는 편이 낫다. 같은 코드 문자열 `TOPOLOGY_MISMATCH` 를 두 예외 타입이 쓰는 것도 정리 대상이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
두 스택을 실제 브로커 토폴로지에 걸어 판정 차이를 관측하지 않았다. BrokerTopologyInspector 의 구현이 저장소에 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-runtime-f10
|
||||
title: 실패한 리드라이브 항목의 사유가 어디에도 남지 않는다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-runtime-f10
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-runtime-f10
|
||||
file: ../../../final/evidence/rendered/messaging-admin-runtime-f10.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-runtime-f10.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-runtime.md#L961 이다.
|
||||
module: messaging-admin-runtime
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 실패한 리드라이브 항목의 사유가 어디에도 남지 않는다
|
||||
|
||||
attempt(...) 는 예외와 미확인을 모두 false 로 접는다(RedriveService:142-156). 감사 이벤트는 failed 개수만 담는다(:135).
|
||||
|
||||
## 문제
|
||||
|
||||
attempt(...) 는 예외와 미확인을 모두 false 로 접는다(RedriveService:142-156).
|
||||
|
||||
감사 이벤트는 failed 개수만 담는다(:135).
|
||||
|
||||
## 결론
|
||||
|
||||
사건 복구 중에 "왜 이 메시지들이 안 갔는가" 를 물을 수 있어야 하는데 답이 없다.
|
||||
|
||||
RedriveReport 에 실패 사유별 집계(코드 → 개수) 정도만 추가해도 크게 달라진다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : RedriveReport 참조 10건 검색과 실패 항목이 접히는 지점 및 감사 이벤트가 담는 값 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-runtime.md#L961 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`attempt(...)` 는 예외와 미확인을 모두 `false` 로 접는다(`RedriveService:142-156`). 감사 이벤트는 `failed` 개수만 담는다(`:135`).
|
||||
|
||||
## RedriveReport 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f10" alt="코드베이스에서 RedriveReport 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedriveReport 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 사건 복구 중에 물을 수 있어야 하는 질문
|
||||
|
||||
"왜 이 메시지들이 안 갔는가" 인데 답이 없다. `RedriveReport` 에 실패 사유별 집계(코드 → 개수) 정도만 추가해도 크게 달라진다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
사건 복구 중에 실패 사유를 되묻는 상황을 재현하지 않았다. attempt 가 예외와 미확인을 같은 값으로 접는다는 코드로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-core-api-f02
|
||||
title: 배치 metadata를 만들고 넘길 곳이 없다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-core-api-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-core-api-f02
|
||||
file: ../../../final/evidence/rendered/messaging-core-api-f02.svg
|
||||
- key: messaging-core-api-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-core-api-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-core-api-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-core-api.md#L844 이다.
|
||||
module: messaging-core-api
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 배치 metadata를 만들고 넘길 곳이 없다
|
||||
|
||||
KafkaBatchConsumerRegistrar:104와 RabbitBatchConsumerRegistrar:139가 BatchDeliveryMetadata를 만들고, 두 javadoc 다 "the batch-wide metadata handed to the handler"라고 적는다. new BatchMessageDelivery는 저장소 전체에서 0건이고 BatchMessageHandler 참조도 0건이다.
|
||||
|
||||
## 문제
|
||||
|
||||
KafkaBatchConsumerRegistrar:104와 RabbitBatchConsumerRegistrar:139가 BatchDeliveryMetadata를 만들고, 두 javadoc 다 "the batch-wide metadata handed to the handler"라고 적는다.
|
||||
|
||||
new BatchMessageDelivery는 저장소 전체에서 0건이고 BatchMessageHandler 참조도 0건이다.
|
||||
|
||||
## 결론
|
||||
|
||||
두 registrar는 브로커별로 다른 정확한 계산을 한다 — Kafka는 파티션 단위 커밋이라 settlableAsBatch=true, Rabbit은 multiple-ack이 in-flight까지 정산하므로 false.
|
||||
|
||||
이 판단이 계산되어 어디에도 전달되지 않는다.
|
||||
|
||||
javadoc은 존재하지 않는 수신자를 가리킨다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : BatchDeliveryMetadata 참조 10건 검색과 그것을 받을 타입의 생성 지점 수 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-core-api.md#L844 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`KafkaBatchConsumerRegistrar:104`와 `RabbitBatchConsumerRegistrar:139`가 `BatchDeliveryMetadata`를 만들고, 두 javadoc 다 "the batch-wide metadata handed to the handler"라고 적는다.
|
||||
|
||||
## 만들고 넘길 곳이 없는 값
|
||||
|
||||
:::evidence key="messaging-core-api-f02-diagram" alt="registrar 의 계산만 이 값이 도달하는 범위 안에 놓이고 BatchMessageHandler 가 바깥에 빗금으로 놓인다" caption="만들고 넘길 곳이 없는 값" zoom="false"
|
||||
:::
|
||||
|
||||
`new BatchMessageDelivery`는 저장소 전체에서 0건이고 `BatchMessageHandler` 참조도 0건이다.
|
||||
|
||||
## BatchDeliveryMetadata 참조 위치
|
||||
|
||||
:::evidence key="messaging-core-api-f02" alt="코드베이스에서 BatchDeliveryMetadata 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BatchDeliveryMetadata 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 브로커별로 다른 정확한 계산이 버려진다
|
||||
|
||||
Kafka는 파티션 단위 커밋이라 `settlableAsBatch=true`, Rabbit은 multiple-ack이 in-flight까지 정산하므로 `false`. 이 판단이 계산되어 어디에도 전달되지 않고, javadoc은 존재하지 않는 수신자를 가리킨다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
저장소 밖 소비자가 이 타입을 구현하는지 확인할 방법이 이 저장소 안에 없다. 이 판단이 그 미지수에 걸려 있다.
|
||||
|
||||
<!-- body:end -->
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-inbox-jdbc-postgresql-f03
|
||||
title: SQL 실패가 재시도 불가로 분류된다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-inbox-jdbc-postgresql-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-inbox-jdbc-postgresql-f03
|
||||
file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-f03.svg
|
||||
- key: messaging-inbox-jdbc-postgresql-f03-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-inbox-jdbc-postgresql-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-inbox-jdbc-postgresql.md#L666 이다.
|
||||
module: messaging-inbox-jdbc-postgresql
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# SQL 실패가 재시도 불가로 분류된다
|
||||
|
||||
INBOX_RESERVE_FAILED·INBOX_QUERY_FAILED·INBOX_PURGE_FAILED 셋 다 MessagingConfigurationException이고, 그 예외의 카테고리는 CONFIGURATION, retryable = false다. SQLException의 원인 대부분은 구성 오류가 아니라 일시적 인프라다 — 연결 끊김, 데드락, 락 타임아웃, 커넥션 풀 고갈.
|
||||
|
||||
## 관계
|
||||
|
||||
- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
INBOX_RESERVE_FAILED·INBOX_QUERY_FAILED·INBOX_PURGE_FAILED 셋 다 MessagingConfigurationException이고, 그 예외의 카테고리는 CONFIGURATION, retryable = false다.
|
||||
|
||||
SQLException의 원인 대부분은 구성 오류가 아니라 일시적 인프라다 — 연결 끊김, 데드락, 락 타임아웃, 커넥션 풀 고갈.
|
||||
|
||||
## 결론
|
||||
|
||||
FailureCategory는 "the stable classification a retry engine, DLQ router, and dashboard all agree on"이고 retryable = false는 재시도 엔진이 즉시 파킹한다는 뜻이다.
|
||||
|
||||
같은 leaf의 INBOX_ACTION_FAILED는 TRANSIENT_INFRASTRUCTURE/retryable = true로 정확히 분류된다 — 같은 파일 안에서 기준이 갈린다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 세 코드가 쓰는 예외 타입과 그 예외의 카테고리·재시도 가능 여부 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-inbox-jdbc-postgresql.md#L666 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`INBOX_RESERVE_FAILED`·`INBOX_QUERY_FAILED`·`INBOX_PURGE_FAILED` 셋 다 `MessagingConfigurationException`이고, 그 예외의 카테고리는 `CONFIGURATION`, `retryable = false`다.
|
||||
|
||||
## 같은 파일 안에서 갈리는 분류
|
||||
|
||||
:::evidence key="messaging-inbox-jdbc-postgresql-f03-diagram" alt="SQLException 에서 세 코드는 CONFIGURATION 이고 INBOX_ACTION_FAILED 는 TRANSIENT 인 두 갈래가 나온다" caption="같은 파일 안에서 갈리는 분류" zoom="false"
|
||||
:::
|
||||
|
||||
`SQLException`의 원인 대부분은 구성 오류가 아니라 **일시적 인프라**다 — 연결 끊김, 데드락, 락 타임아웃, 커넥션 풀 고갈.
|
||||
|
||||
## 세 코드가 공유하는 예외 타입
|
||||
|
||||
:::evidence key="messaging-inbox-jdbc-postgresql-f03" alt="분석 문서 analysis/messaging/messaging-inbox-jdbc-postgresql.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-inbox-jdbc-postgresql.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 세 소비자의 합의가 깨진다
|
||||
|
||||
`FailureCategory`는 "the stable classification a retry engine, DLQ router, and dashboard all agree on"이고 `retryable = false`는 재시도 엔진이 즉시 파킹한다는 뜻이다. 같은 leaf의 `INBOX_ACTION_FAILED`는 `TRANSIENT_INFRASTRUCTURE`/`retryable = true`로 정확히 분류된다 — 같은 파일 안에서 기준이 갈린다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
일시적 인프라 오류를 실제로 일으켜 메시지가 파킹되는 것을 관측하지 않았다. 예외 타입과 카테고리의 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-kafka-f02
|
||||
title: 천장에 닿아 일시정지된 파티션을 재개하는 경로가 없다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-kafka-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-kafka-f02
|
||||
file: ../../../final/evidence/rendered/messaging-kafka-f02.svg
|
||||
- key: messaging-kafka-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-kafka-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-kafka-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-kafka.md#L265 이다.
|
||||
module: messaging-kafka
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 천장에 닿아 일시정지된 파티션을 재개하는 경로가 없다
|
||||
|
||||
pollOnce 의 파티션 루프는 세 경우에 그 파티션을 멈춘다. 이 세 경로 중 어느 것도 retries.pauseUntil(...) 을 부르지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
pollOnce 의 파티션 루프는 세 경우에 그 파티션을 멈춘다.
|
||||
|
||||
이 세 경로 중 어느 것도 retries.pauseUntil(...) 을 부르지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
그런데 폴 루프가 파티션을 재개하는 곳은 하나뿐이다.
|
||||
|
||||
retries 에 항목을 넣는 곳은 QueuedSettlement.enqueueRequeue 하나이고, 그것은 핸들러 실패·타임아웃·명시적 requeue 경로다.
|
||||
|
||||
천장·배수·풀 거부 경로는 등록하지 않는다.
|
||||
|
||||
따라서 천장 때문에 멈춘 파티션은 폴 루프가 스스로 재개하지 않는다.
|
||||
|
||||
재개할 수 있는 것은 외부에서 부른 resume(scope) 이나 재조정뿐이다.
|
||||
|
||||
maxInFlightPerOrderingUnit 의 기본값은 1 이다(DestinationSettings.Consumer).
|
||||
|
||||
한 폴이 같은 파티션의 레코드를 둘 이상 돌려주는 순간 두 번째에서 tryAcquire 가 거짓이 되고, 그 파티션이 멈춘다.
|
||||
|
||||
그 뒤 작업자가 끝나 coordinator.release 로 슬롯이 비어도 consumer 는 여전히 일시정지 상태다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : QueuedSettlement 참조 3건 검색과 파티션을 멈추는 세 경로의 재개 등록 여부 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-kafka.md#L265 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`pollOnce` 의 파티션 루프는 세 경우에 그 파티션을 멈춘다 — 천장(`tryAcquire` 실패), 배수 시작(`tryBeginWork` 실패), 풀 거부(`dispatch` 실패). 이 세 경로 중 어느 것도 `retries.pauseUntil(...)` 을 부르지 않는다.
|
||||
|
||||
## 재개가 끊긴 자리
|
||||
|
||||
:::evidence key="messaging-kafka-f02-diagram" alt="핸들러 실패와 타임아웃과 requeue 만 재개 목록에 등록되는 경로 안에 놓이고 천장 도달과 배수 및 풀 거부가 바깥에 빗금으로 놓인다" caption="재개가 끊긴 자리" zoom="false"
|
||||
:::
|
||||
|
||||
폴 루프가 파티션을 재개하는 곳은 `applyDueResumes` 하나이고 그것은 `retries.dueForResume(now)` 만 본다. `retries` 에 항목을 넣는 곳은 `QueuedSettlement.enqueueRequeue` 하나이고, 그것은 핸들러 실패·타임아웃·명시적 requeue 경로다.
|
||||
|
||||
## QueuedSettlement 참조 위치
|
||||
|
||||
:::evidence key="messaging-kafka-f02" alt="코드베이스에서 QueuedSettlement 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="QueuedSettlement 코드베이스 검색 — 3줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 기본값이 이 경로를 흔하게 만든다
|
||||
|
||||
`maxInFlightPerOrderingUnit` 의 기본값은 1 이다(`DestinationSettings.Consumer`). 한 폴이 같은 파티션의 레코드를 둘 이상 돌려주는 순간 두 번째에서 `tryAcquire` 가 거짓이 되고 그 파티션이 멈춘다. 그 뒤 작업자가 끝나 `coordinator.release` 로 슬롯이 비어도 `consumer` 는 여전히 일시정지 상태다.
|
||||
|
||||
## 두 pause 가 구분되어 쓰인다
|
||||
|
||||
`applySettlements` 의 `PAUSE_AND_SEEK` 는 `coordinator.pause(...)` 와 `consumer.pause(...)` 를 둘 다 부르고, 천장 경로는 `consumer` 쪽만 부른다. 그래서 조정자는 그 파티션을 멈춘 것으로 알지 못하고, `tryAcquire` 는 계속 참을 답하는데 브로커에서 레코드가 오지 않는다.
|
||||
|
||||
## 수정
|
||||
|
||||
천장 경로가 `retries.pauseUntil(partition, seekBackTo, Duration.ZERO, now)` 를 등록하면 다음 주기의 `applyDueResumes` 가 즉시 재개한다 — 지연이 0 이므로 `dueForResume` 이 곧바로 돌려준다. 배수 경로는 재개하지 않는 것이 맞고, 풀 거부 경로는 천장과 같다. 소비 경로가 조립되지 않으므로(§12.1) P2.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실행으로 재현하지 않았다. resume 호출처가 둘뿐이고 천장 경로가 재개 목록에 아무것도 등록하지 않는다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-kafka-f03
|
||||
title: 오염된 재시도 헤더가 격리되지 않고 무한 pause-and-seek 을 만든다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-kafka-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-kafka-f03
|
||||
file: ../../../final/evidence/rendered/messaging-kafka-f03.svg
|
||||
- key: messaging-kafka-f03-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-kafka-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-kafka-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-kafka.md#L301 이다.
|
||||
module: messaging-kafka
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 오염된 재시도 헤더가 격리되지 않고 무한 pause-and-seek 을 만든다
|
||||
|
||||
KafkaRetryMetadataMapper.attemptOf 는 읽을 수 없는 msg.retry.attempt 에 대해 fail-closed 를 택하고, 그 이유를 정확하게 적는다. 메시지가 "quarantined" 라고 말한다.
|
||||
|
||||
## 문제
|
||||
|
||||
KafkaRetryMetadataMapper.attemptOf 는 읽을 수 없는 msg.retry.attempt 에 대해 fail-closed 를 택하고, 그 이유를 정확하게 적는다.
|
||||
|
||||
메시지가 "quarantined" 라고 말한다.
|
||||
|
||||
## 결론
|
||||
|
||||
소비자는 그렇게 하지 않는다.
|
||||
|
||||
격리 경로는 디코딩 실패에만 걸려 있다.
|
||||
|
||||
attemptOf 는 디코딩이 끝난 뒤 두 번째 블록에서 던지고, MessagingConfigurationException 은 MessagingException 을 통해 RuntimeException 이므로 두 번째 catch 가 잡는다.
|
||||
|
||||
결과는 requeueAfterFailure() → PAUSE_AND_SEEK → 같은 오프셋 재읽기 → 같은 헤더 → 같은 예외다.
|
||||
|
||||
즉 fail-closed 가 막으려던 것(재시도 예산 무력화)보다 나쁜 것을 만든다 — 그 파티션이 영구히 그 레코드에서 멈춘다.
|
||||
|
||||
그리고 javadoc 이 지적한 대로 이 헤더는 호출자가 쓸 수 있는 값이므로, 숫자가 아닌 값 하나로 파티션 하나를 정지시킬 수 있다.
|
||||
|
||||
ReservedHeaderForgeryTest.aMalformedRetryAttemptIsQuarantined 는 attemptOf 가 던지는 것만 단언한다.
|
||||
|
||||
이름은 "quarantined" 인데 격리를 확인하지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : KafkaRetryMetadataMapper 참조 9건 검색과 던지는 지점이 놓인 try·catch 범위 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-kafka.md#L301 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`KafkaRetryMetadataMapper.attemptOf` 는 읽을 수 없는 `msg.retry.attempt` 에 대해 fail-closed 를 택하고, 메시지가 "quarantined" 라고 말한다. 소비자는 그렇게 하지 않는다 — 격리 경로는 **디코딩 실패에만** 걸려 있다.
|
||||
|
||||
## 격리 대신 도는 루프
|
||||
|
||||
:::evidence key="messaging-kafka-f03-diagram" alt="숫자 아닌 헤더가 attemptOf 던짐과 두 번째 catch 를 지나 같은 오프셋 재읽기로 이어진다" caption="격리 대신 도는 루프" zoom="false"
|
||||
:::
|
||||
|
||||
`attemptOf` 는 디코딩이 끝난 뒤 두 번째 블록에서 던지고, `MessagingConfigurationException` 은 `MessagingException` 을 통해 `RuntimeException` 이므로 두 번째 `catch` 가 잡는다. 결과는 `requeueAfterFailure()` → `PAUSE_AND_SEEK` → 같은 오프셋 재읽기 → 같은 헤더 → 같은 예외다.
|
||||
|
||||
## KafkaRetryMetadataMapper 참조 위치
|
||||
|
||||
:::evidence key="messaging-kafka-f03" alt="코드베이스에서 KafkaRetryMetadataMapper 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KafkaRetryMetadataMapper 코드베이스 검색 — 9줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## fail-closed 가 막으려던 것보다 나쁜 것을 만든다
|
||||
|
||||
그 파티션이 영구히 그 레코드에서 멈춘다. 그리고 javadoc 이 지적한 대로 이 헤더는 호출자가 쓸 수 있는 값이므로, 숫자가 아닌 값 하나로 파티션 하나를 정지시킬 수 있다.
|
||||
|
||||
## 테스트가 이름과 다른 것을 본다
|
||||
|
||||
`ReservedHeaderForgeryTest.aMalformedRetryAttemptIsQuarantined` 는 `attemptOf` 가 던지는 것만 단언한다. 이름은 "quarantined" 인데 격리를 확인하지 않는다.
|
||||
|
||||
## 수정
|
||||
|
||||
`attemptOf` 호출을 디코딩과 같은 블록으로 옮겨 격리 경로에 태우거나, 두 번째 `catch` 가 예외 종류를 나누게 한다 — `MessagingConfigurationException` 은 재시도로 회복되지 않는 종류이므로 격리 대상이고, 핸들러 실패는 재시도 대상이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실행으로 재현하지 않았다. attemptOf 가 dispatch 의 두 번째 try 안에 있고 그 catch 가 재요청이라는 것, 그리고 그 예외가 RuntimeException 을 상속한다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-kafka-f04
|
||||
title: 시계를 주입받는 클래스가 한 곳에서만 벽시계를 읽는다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-kafka-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-kafka-f04
|
||||
file: ../../../final/evidence/rendered/messaging-kafka-f04.svg
|
||||
- key: messaging-kafka-f04-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-kafka-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-kafka-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-kafka.md#L342 이다.
|
||||
module: messaging-kafka
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 시계를 주입받는 클래스가 한 곳에서만 벽시계를 읽는다
|
||||
|
||||
KafkaConsumerRegistrar 의 설계 성질이 javadoc 에 적혀 있다. 주기마다 Instant now 를 받아 applyDueResumes(now) 로 넘긴다.
|
||||
|
||||
## 문제
|
||||
|
||||
KafkaConsumerRegistrar 의 설계 성질이 javadoc 에 적혀 있다.
|
||||
|
||||
주기마다 Instant now 를 받아 applyDueResumes(now) 로 넘긴다.
|
||||
|
||||
## 결론
|
||||
|
||||
그런데 그 짝인 등록 쪽은 이렇다.
|
||||
|
||||
이 리프에서 Instant.now() 를 읽는 유일한 자리다.
|
||||
|
||||
그리고 그 호출은 작업자 스레드에서 일어나므로 폴 스레드의 now 와 다른 순간이다.
|
||||
|
||||
결과는 두 가지다.
|
||||
|
||||
지연 재시도(requeue(Duration))의 재개 시점을 고정 시계로 시험할 수 없고, 시험이 Duration.ZERO 밖의 지연을 다루지 못한다 — 실제로 어떤 시험도 다루지 않는다.
|
||||
|
||||
수정은 생성자에 Supplier<Instant> 를 하나 더 받는 것이다.
|
||||
|
||||
같은 저장소의 MessagingShutdownLifecycle 이 정확히 그 형태로 두 생성자를 둔다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : KafkaConsumerRegistrar 참조 5건 검색과 주입된 시계가 덮는 범위 대비 벽시계 호출 지점 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-kafka.md#L342 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`KafkaConsumerRegistrar` 의 설계 성질이 javadoc 에 적혀 있다 — 주기마다 `Instant now` 를 받아 `applyDueResumes(now)` 로 넘긴다.
|
||||
|
||||
## 시계가 닿지 않는 한 곳
|
||||
|
||||
:::evidence key="messaging-kafka-f04-diagram" alt="폴 주기의 now 만 주입된 시계가 덮는 범위 안에 놓이고 등록 쪽 Instant.now 가 바깥에 빗금으로 놓인다" caption="시계가 닿지 않는 한 곳" zoom="false"
|
||||
:::
|
||||
|
||||
그 짝인 등록 쪽이 이 리프에서 `Instant.now()` 를 읽는 유일한 자리다. 그리고 그 호출은 작업자 스레드에서 일어나므로 폴 스레드의 `now` 와 다른 순간이다.
|
||||
|
||||
## KafkaConsumerRegistrar 참조 위치
|
||||
|
||||
:::evidence key="messaging-kafka-f04" alt="코드베이스에서 KafkaConsumerRegistrar 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KafkaConsumerRegistrar 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 결과 둘
|
||||
|
||||
지연 재시도(`requeue(Duration)`)의 재개 시점을 고정 시계로 시험할 수 없고, 시험이 `Duration.ZERO` 밖의 지연을 다루지 못한다 — 실제로 어떤 시험도 다루지 않는다.
|
||||
|
||||
## 수정
|
||||
|
||||
생성자에 `Supplier<Instant>` 를 하나 더 받는 것이다. 같은 저장소의 `MessagingShutdownLifecycle` 이 정확히 그 형태로 두 생성자를 둔다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
시계를 고정해 두 경로의 시각 차이를 관측하지 않았다. 호출 지점의 코드 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-kafka-f05
|
||||
title: 결함으로 판정된 메서드가 남아 있고, 실브로커 증명이 그것 위에서 돈다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-kafka-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-kafka-f05
|
||||
file: ../../../final/evidence/rendered/messaging-kafka-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-kafka-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-kafka.md#L362 이다.
|
||||
module: messaging-kafka
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 결함으로 판정된 메서드가 남아 있고, 실브로커 증명이 그것 위에서 돈다
|
||||
|
||||
KafkaTransactionalPublisher 에 같은 일을 하는 메서드가 둘 있다. inTransaction 의 javadoc 이 둘째를 결함으로 지목한다.
|
||||
|
||||
## 문제
|
||||
|
||||
KafkaTransactionalPublisher 에 같은 일을 하는 메서드가 둘 있다.
|
||||
|
||||
inTransaction 의 javadoc 이 둘째를 결함으로 지목한다.
|
||||
|
||||
## 결론
|
||||
|
||||
sendInTransaction 은 public 이고 production 호출자가 없다.
|
||||
|
||||
호출하는 것은 시험 다섯 자리뿐이다 — 그리고 그 다섯이 실브로커 트랜잭션 증명 전부다(KafkaTransactionIT·KafkaTransactionFencingIT·KafkaReadCommittedIT).
|
||||
|
||||
고쳐진 inTransaction 을 시험하는 것은 KafkaTransactionOrderingTest 하나이고 MockProducer 다.
|
||||
|
||||
즉 실브로커에서 커밋·중단·펜싱이 증명된 것은 옛 모양이고, 새 모양은 목 위에서만 증명됐다.
|
||||
|
||||
기능적 차이는 크지 않다(body 가 비어 있으면 두 메서드는 같은 호출열을 만든다).
|
||||
|
||||
그래도 두 가지가 남는다 — 결함으로 판정된 순서를 만드는 public 진입점이 여전히 열려 있다는 것, 그리고 실브로커 증거가 production 경로가 아닌 것 위에 있다는 것.
|
||||
|
||||
수정은 ITs 를 inTransaction(..., () -> null) 로 옮기고 sendInTransaction 을 지우는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : KafkaTransactionalPublisher 참조 26건 검색과 두 메서드 중 실브로커 증명이 쓰는 쪽 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-kafka.md#L362 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`KafkaTransactionalPublisher` 에 같은 일을 하는 메서드가 둘 있고, `inTransaction` 의 javadoc 이 둘째를 결함으로 지목한다. `sendInTransaction` 은 public 이고 production 호출자가 없다.
|
||||
|
||||
## KafkaTransactionalPublisher 참조 위치
|
||||
|
||||
:::evidence key="messaging-kafka-f05" alt="코드베이스에서 KafkaTransactionalPublisher 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KafkaTransactionalPublisher 코드베이스 검색 — 26줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 실브로커 증명이 옛 모양 위에서 돈다
|
||||
|
||||
호출하는 것은 시험 다섯 자리뿐이고, 그 다섯이 실브로커 트랜잭션 증명 전부다(`KafkaTransactionIT`·`KafkaTransactionFencingIT`·`KafkaReadCommittedIT`). 고쳐진 `inTransaction` 을 시험하는 것은 `KafkaTransactionOrderingTest` 하나이고 `MockProducer` 다.
|
||||
|
||||
## 남는 두 가지
|
||||
|
||||
기능적 차이는 크지 않다 — `body` 가 비어 있으면 두 메서드는 같은 호출열을 만든다. 그래도 결함으로 판정된 순서를 만드는 public 진입점이 여전히 열려 있고, 실브로커 증거가 production 경로가 아닌 것 위에 있다. 수정은 ITs 를 `inTransaction(..., () -> null)` 로 옮기고 `sendInTransaction` 을 지우는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
Toxiproxy 인증 레인을 직접 돌리지 않았다. 코드와 그 레인이 기록하는 증거 형식만 읽었다.
|
||||
|
||||
<!-- body:end -->
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-kafka-share-experimental-f01
|
||||
title: "등록"이 아무것도 등록하지 않고 성공을 반환한다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-kafka-share-experimental-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-kafka-share-experimental-f01
|
||||
file: ../../../final/evidence/rendered/messaging-kafka-share-experimental-f01.svg
|
||||
- key: messaging-kafka-share-experimental-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-kafka-share-experimental-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-kafka-share-experimental-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-kafka-share-experimental.md#L467 이다.
|
||||
module: messaging-kafka-share-experimental
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# "등록"이 아무것도 등록하지 않고 성공을 반환한다
|
||||
|
||||
KafkaShareGroupRegistrar.register(profile, spec)이 spec을 Objects.requireNonNull로만 처리하고 버린다. ShareRegistration은 profile과 AtomicBoolean 둘만 갖는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **구성 오류는 한 예외 타입과 안정 코드로 보고한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
KafkaShareGroupRegistrar.register(profile, spec)이 spec을 Objects.requireNonNull로만 처리하고 버린다.
|
||||
|
||||
ShareRegistration은 profile과 AtomicBoolean 둘만 갖는다.
|
||||
|
||||
## 결론
|
||||
|
||||
Kafka 소비자가 만들어지지 않고(import org.apache.kafka 0건), spec.sink가 저장되지 않으므로 어떤 전달도 일어나지 않는다.
|
||||
|
||||
반환된 registration은 isActive() == true를 보고한다.
|
||||
|
||||
같은 클래스의 javadoc이 pause를 조용히 무시하는 것을 거절한 이유로 "would let a retry policy that depends on pausing appear to work while doing nothing"을 든다.
|
||||
|
||||
register 자체가 정확히 그 형태다 — 성공을 반환하고 아무것도 하지 않으며 isActive()가 true다.
|
||||
|
||||
오늘 호출자가 없으므로 사고는 아니지만, 이 leaf를 배선하는 사람이 가장 먼저 부를 메서드다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : register 본문에서 spec 이 보관되는 필드 유무와 sink 호출 지점 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-kafka-share-experimental.md#L467 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`KafkaShareGroupRegistrar.register(profile, spec)`이 `spec`을 `Objects.requireNonNull`로만 처리하고 버린다.
|
||||
|
||||
## 등록이 버리는 것
|
||||
|
||||
:::evidence key="messaging-kafka-share-experimental-f01-diagram" alt="profile 과 AtomicBoolean 만 register 가 보관하는 것 안에 놓이고 spec 과 sink 가 바깥에 빗금으로 놓인다" caption="등록이 버리는 것" zoom="false"
|
||||
:::
|
||||
|
||||
`ShareRegistration`은 `profile`과 `AtomicBoolean` 둘만 갖는다. Kafka 소비자가 만들어지지 않고(`import org.apache.kafka` 0건), `spec.sink`가 저장되지 않으므로 어떤 전달도 일어나지 않는다.
|
||||
|
||||
## register 가 spec 에 하는 일
|
||||
|
||||
:::evidence key="messaging-kafka-share-experimental-f01" alt="분석 문서 analysis/messaging/messaging-kafka-share-experimental.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-kafka-share-experimental.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 같은 클래스가 이 형태를 거절한 적이 있다
|
||||
|
||||
반환된 registration은 `isActive() == true`를 보고한다. 같은 클래스의 javadoc이 pause를 조용히 무시하는 것을 거절한 이유로 "would let a retry policy that depends on pausing appear to work while doing nothing"을 든다. `register` 자체가 정확히 그 형태다 — 성공을 반환하고 아무것도 하지 않으며 `isActive()`가 true다.
|
||||
|
||||
## 오늘 호출자는 없다
|
||||
|
||||
사고는 아니지만, 이 leaf를 배선하는 사람이 가장 먼저 부를 메서드다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이 리프를 완성할 계획이 있는지 확인할 수 없었다. 커밋이 대량 커밋뿐이고 기록이 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-nats-experimental-f02
|
||||
title: 닫힌 전송의 거절이 영구 업무 실패로 분류된다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-nats-experimental-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-nats-experimental-f02
|
||||
file: ../../../final/evidence/rendered/messaging-nats-experimental-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-nats-experimental-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-nats-experimental.md#L209 이다.
|
||||
module: messaging-nats-experimental
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 닫힌 전송의 거절이 영구 업무 실패로 분류된다
|
||||
|
||||
rejectedLocally 가 FailureCategory.PERMANENT_BUSINESS 를 고정으로 쓰고, 두 호출자 중 하나가 NATS_TRANSPORT_CLOSED 다. 자매 어댑터(Pulsar)와 같은 형태이고 같은 판단이다 — 종료 중이라는 것은 이 세대의 사정이지 업무의 영구 실패가 아니다.
|
||||
|
||||
## 문제
|
||||
|
||||
rejectedLocally 가 FailureCategory.PERMANENT_BUSINESS 를 고정으로 쓰고, 두 호출자 중 하나가 NATS_TRANSPORT_CLOSED 다.
|
||||
|
||||
자매 어댑터(Pulsar)와 같은 형태이고 같은 판단이다 — 종료 중이라는 것은 이 세대의 사정이지 업무의 영구 실패가 아니다.
|
||||
|
||||
## 결론
|
||||
|
||||
같은 파일의 classify 는 범주를 신중히 나눈다.
|
||||
|
||||
두 리프가 같은 형태를 공유하므로 수정도 함께 하는 편이 낫다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : rejectedLocally 가 붙이는 범주와 두 호출자의 실패 성격 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-nats-experimental.md#L209 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`rejectedLocally` 가 `FailureCategory.PERMANENT_BUSINESS` 를 고정으로 쓰고, 두 호출자 중 하나가 `NATS_TRANSPORT_CLOSED` 다.
|
||||
|
||||
## rejectedLocally 가 붙이는 범주
|
||||
|
||||
:::evidence key="messaging-nats-experimental-f02" alt="분석 문서 analysis/messaging/messaging-nats-experimental.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-nats-experimental.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 자매 어댑터와 같은 형태이고 같은 판단이다
|
||||
|
||||
Pulsar 쪽도 그렇다 — 종료 중이라는 것은 이 세대의 사정이지 업무의 영구 실패가 아니다.
|
||||
|
||||
## 같은 파일의 classify 는 범주를 신중히 나눈다
|
||||
|
||||
두 리프가 같은 형태를 공유하므로 수정도 함께 하는 편이 낫다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
종료 중 거절을 실제로 발생시켜 재시도 정책의 차이를 관측하지 않았다. 같은 파일의 분류기가 범주를 나누는 것과의 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-nats-experimental-f03
|
||||
title: NatsJetStreamProfileValidator 를 부르는 곳이 javadoc 링크 하나뿐이다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-nats-experimental-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-nats-experimental-f03
|
||||
file: ../../../final/evidence/rendered/messaging-nats-experimental-f03.svg
|
||||
- key: messaging-nats-experimental-f03-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-nats-experimental-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-nats-experimental-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-nats-experimental.md#L217 이다.
|
||||
module: messaging-nats-experimental
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# NatsJetStreamProfileValidator 를 부르는 곳이 javadoc 링크 하나뿐이다
|
||||
|
||||
75줄짜리 검증기가 이 어댑터의 시작 시점 판단 넷을 들고 있다 — 실험 스위치가 꺼져 있으면 거부, 최소 한 번 배달에 코어 NATS 거부, 순서 있는 소비자와 경쟁 작업자 동시 사용 거부, 키 순서 목적지 거부. 저장소 전역에서 이 클래스 이름이 나오는 곳은 두 줄뿐이다.
|
||||
|
||||
## 문제
|
||||
|
||||
75줄짜리 검증기가 이 어댑터의 시작 시점 판단 넷을 들고 있다 — 실험 스위치가 꺼져 있으면 거부, 최소 한 번 배달에 코어 NATS 거부, 순서 있는 소비자와 경쟁 작업자 동시 사용 거부, 키 순서 목적지 거부.
|
||||
|
||||
저장소 전역에서 이 클래스 이름이 나오는 곳은 두 줄뿐이다.
|
||||
|
||||
## 결론
|
||||
|
||||
하나는 선언이고 하나는 javadoc 링크다.
|
||||
|
||||
코드 호출자 0, 테스트 0.
|
||||
|
||||
validate 는 jetStreamEnabled · orderedConsumer · competingWorkers · enabled 를 전부 인자로 받는다.
|
||||
|
||||
즉 스스로 아무것도 관찰하지 않고, 호출자가 이미 알고 있는 사실을 넘겨 줘야만 판단한다.
|
||||
|
||||
그런 호출자가 없으니 이 판단들은 한 번도 실행된 적이 없다.
|
||||
|
||||
전송의 클래스 javadoc 이 "그래서 검증기가 시작 시 그 조합을 거부한다"고 단언한다.
|
||||
|
||||
읽는 사람에게 이 어댑터는 코어 NATS 오설정으로부터 보호되는 것처럼 보이는데, 실제로는 아무 게이트도 걸려 있지 않다.
|
||||
|
||||
실험 등급이라 지금 당장 사고가 나지는 않지만, 이 어댑터를 실전에 붙이는 사람이 가장 먼저 신뢰할 문장이 지금 사실이 아니다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : NatsJetStreamProfileValidator 참조 2건 검색으로 선언과 javadoc 링크 외의 호출 유무 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-nats-experimental.md#L217 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
75줄짜리 검증기가 이 어댑터의 시작 시점 판단 넷을 들고 있다 — 실험 스위치가 꺼져 있으면 거부, 최소 한 번 배달에 코어 NATS 거부, 순서 있는 소비자와 경쟁 작업자 동시 사용 거부, 키 순서 목적지 거부.
|
||||
|
||||
## 검증기를 가리키는 두 줄
|
||||
|
||||
:::evidence key="messaging-nats-experimental-f03-diagram" alt="선언과 javadoc 링크만 검증기를 가리키는 것 안에 놓이고 코드 호출자와 테스트가 바깥에 빗금으로 놓인다" caption="검증기를 가리키는 두 줄" zoom="false"
|
||||
:::
|
||||
|
||||
저장소 전역에서 이 클래스 이름이 나오는 곳은 두 줄뿐이다 — 하나는 선언이고 하나는 **javadoc 링크**다. 코드 호출자 0, 테스트 0.
|
||||
|
||||
## NatsJetStreamProfileValidator 참조 위치
|
||||
|
||||
:::evidence key="messaging-nats-experimental-f03" alt="코드베이스에서 NatsJetStreamProfileValidator 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NatsJetStreamProfileValidator 코드베이스 검색 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 스스로 아무것도 관찰하지 않는다
|
||||
|
||||
`validate` 는 `jetStreamEnabled` · `orderedConsumer` · `competingWorkers` · `enabled` 를 전부 인자로 받는다 — 호출자가 이미 알고 있는 사실을 넘겨 줘야만 판단한다. 그런 호출자가 없으니 이 판단들은 한 번도 실행된 적이 없다.
|
||||
|
||||
## 읽는 사람에게는 보호되는 것처럼 보인다
|
||||
|
||||
전송의 클래스 javadoc 이 "그래서 검증기가 시작 시 그 조합을 거부한다"고 단언한다. 실험 등급이라 지금 당장 사고가 나지는 않지만, 이 어댑터를 실전에 붙이는 사람이 가장 먼저 신뢰할 문장이 지금 사실이 아니다.
|
||||
|
||||
## 같은 형태를 여러 번 봤고 이쪽이 더 나쁘다
|
||||
|
||||
`GrpcRawApiImportRule` · `GrpcApplicationBoundaryRules` · `GrpcNettyParityContract` 등은 최소한 테스트가 리터럴을 먹여 판단 자체는 실행해 본다. 여기서는 그것조차 없다. 수정은 어댑터 조립 지점에서 `validate` 를 부르거나, 그럴 지점이 아직 없다면 프로파일 생성 시점에 걸리도록 옮기는 것이다. 어느 쪽도 못 하겠다면 전송 javadoc 의 "refuses ... at startup" 을 사실에 맞게 고친다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
리플렉션이나 문자열 기반 조립으로 이 검증기를 부르는 형태는 배제하지 못했다. 클래스 이름과 패키지 이름 두 가지 grep 으로만 확인했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-policy-f01
|
||||
title: 재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-policy-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-policy-f01
|
||||
file: ../../../final/evidence/rendered/messaging-policy-f01.svg
|
||||
- key: messaging-policy-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-policy-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-policy-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-policy.md#L781 이다.
|
||||
module: messaging-policy
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다
|
||||
|
||||
MessagingCoreAutoConfiguration이 RetryDecisionEngine(:167)과 DeadLetterOrchestrator(:179)를 @Bean @ConditionalOnMissingBean으로 만든다. 두 타입을 받는 production 코드는 각각 KafkaRetryExecutor와 KafkaDeadLetterPublisher/RabbitDeadLetterPublisher뿐이고, 셋 다 저장소 어디에서도 생성되지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **구성 오류는 한 예외 타입과 안정 코드로 보고한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **저장소 밖 문서를 절 번호로 인용하지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **부팅 경로의 알고리즘 복잡도는 문서화한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
MessagingCoreAutoConfiguration이 RetryDecisionEngine(:167)과 DeadLetterOrchestrator(:179)를 @Bean @ConditionalOnMissingBean으로 만든다.
|
||||
|
||||
두 타입을 받는 production 코드는 각각 KafkaRetryExecutor와 KafkaDeadLetterPublisher/RabbitDeadLetterPublisher뿐이고, 셋 다 저장소 어디에서도 생성되지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
같은 설정의 51개 bean 중 두 타입을 인자로 받는 @Bean 메서드가 없다.
|
||||
|
||||
컨텍스트에 두 bean이 앉아 있고 MessagingAutoConfigurationTest류의 hasSingleBean 검사는 통과한다 — 즉 bean 존재 검사가 배선을 증명하지 않는다.
|
||||
|
||||
그리고 이 leaf가 가장 공들인 두 축(6개 재시도 모드·8단 판단 순서·full jitter·capability 인식, DLQ 발행-후-정산 불변식·예약 헤더 6개)이 실행되지 않는다.
|
||||
|
||||
42개 테스트 중 16개가 이 두 축을 검증한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : MessagingCoreAutoConfiguration 참조 8건 검색과 두 빈을 인자로 받는 Bean 메서드 유무 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-policy.md#L781 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`MessagingCoreAutoConfiguration`이 `RetryDecisionEngine`(\:167)과 `DeadLetterOrchestrator`(\:179)를 `@Bean @ConditionalOnMissingBean`으로 만든다.
|
||||
|
||||
## 만들어지고 쓰이지 않는 빈
|
||||
|
||||
:::evidence key="messaging-policy-f01-diagram" alt="RetryDecisionEngine 과 DeadLetterOrchestrator 가 컨텍스트에 있는 것 안에 놓이고 두 빈을 받는 Bean 메서드가 바깥에 빗금으로 놓인다" caption="만들어지고 쓰이지 않는 빈" zoom="false"
|
||||
:::
|
||||
|
||||
두 타입을 받는 production 코드는 각각 `KafkaRetryExecutor`와 `KafkaDeadLetterPublisher`/`RabbitDeadLetterPublisher`뿐이고, **셋 다 저장소 어디에서도 생성되지 않는다.** 같은 설정의 51개 bean 중 두 타입을 인자로 받는 `@Bean` 메서드가 없다.
|
||||
|
||||
## MessagingCoreAutoConfiguration 참조 위치
|
||||
|
||||
:::evidence key="messaging-policy-f01" alt="코드베이스에서 MessagingCoreAutoConfiguration 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingCoreAutoConfiguration 코드베이스 검색 — 8줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## bean 존재 검사가 배선을 증명하지 않는다
|
||||
|
||||
컨텍스트에 두 bean이 앉아 있고 `MessagingAutoConfigurationTest`류의 `hasSingleBean` 검사는 통과한다. 그리고 이 leaf가 가장 공들인 두 축(6개 재시도 모드·8단 판단 순서·full jitter·capability 인식, DLQ 발행-후-정산 불변식·예약 헤더 6개)이 실행되지 않는다. 42개 테스트 중 16개가 이 두 축을 검증한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
소비 경로를 배선할 계획이 있는지 확인할 수 없었다. 저장소 안에 답이 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-policy-f03
|
||||
title: 재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-policy-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-policy-f03
|
||||
file: ../../../final/evidence/rendered/messaging-policy-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-policy-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-policy.md#L799 이다.
|
||||
module: messaging-policy
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다
|
||||
|
||||
재시도: DefaultRetryDecisionEngine(6모드·백오프·순서 인식) vs DefaultDeliveryProcessor(고정 지연·시도 횟수 미확인). DLQ: DeadLetterOrchestrator(예약 헤더 6개 부착) vs DefaultDeliveryProcessor.DeadLetterPublisher(헤더 없음).
|
||||
|
||||
## 관계
|
||||
|
||||
- **구성 오류는 한 예외 타입과 안정 코드로 보고한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **저장소 밖 문서를 절 번호로 인용하지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **부팅 경로의 알고리즘 복잡도는 문서화한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
재시도: DefaultRetryDecisionEngine(6모드·백오프·순서 인식) vs DefaultDeliveryProcessor(고정 지연·시도 횟수 미확인).
|
||||
|
||||
DLQ: DeadLetterOrchestrator(예약 헤더 6개 부착) vs DefaultDeliveryProcessor.DeadLetterPublisher(헤더 없음).
|
||||
|
||||
## 결론
|
||||
|
||||
둘 다 조립되지 않았다.
|
||||
|
||||
오늘 경쟁하지 않지만, 소비 경로를 배선하는 사람이 둘 중 하나를 고르게 되고 코드가 어느 쪽이 정본인지 말하지 않는다.
|
||||
|
||||
두 javadoc이 각각 자기가 플랫폼 규칙의 구현이라고 서술한다.
|
||||
|
||||
그리고 선택 결과가 다르다 — DefaultDeliveryProcessor 경로로 DLQ된 메시지에는 실패 카테고리·코드·원본 목적지·시도 횟수가 붙지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : DefaultRetryDecisionEngine 참조 6건 검색과 두 구현의 javadoc·분기 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-policy.md#L799 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
재시도와 DLQ 각각에 구현이 둘이다.
|
||||
|
||||
| 축 | 한쪽 | 다른쪽 |
|
||||
|---|---|---|
|
||||
| 재시도 | `DefaultRetryDecisionEngine`(6모드·백오프·순서 인식) | `DefaultDeliveryProcessor`(고정 지연·시도 횟수 미확인) |
|
||||
| DLQ | `DeadLetterOrchestrator`(예약 헤더 6개 부착) | `DefaultDeliveryProcessor.DeadLetterPublisher`(헤더 없음) |
|
||||
|
||||
## DefaultRetryDecisionEngine 참조 위치
|
||||
|
||||
:::evidence key="messaging-policy-f03" alt="코드베이스에서 DefaultRetryDecisionEngine 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultRetryDecisionEngine 코드베이스 검색 — 6줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 둘 다 조립되지 않았다
|
||||
|
||||
오늘 경쟁하지 않지만, 소비 경로를 배선하는 사람이 둘 중 하나를 고르게 되고 코드가 어느 쪽이 정본인지 말하지 않는다. 두 javadoc이 각각 자기가 플랫폼 규칙의 구현이라고 서술한다.
|
||||
|
||||
## 선택 결과가 다르다
|
||||
|
||||
`DefaultDeliveryProcessor` 경로로 DLQ된 메시지에는 실패 카테고리·코드·원본 목적지·시도 횟수가 붙지 않는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
두 재시도 구현 중 어느 쪽이 정본인지 확인할 수 없었다. 소비 경로 배선 계획이라는 같은 미지수에 걸린다.
|
||||
|
||||
<!-- body:end -->
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-policy-f04
|
||||
title: DLQ 메타데이터의 두 시각이 항상 같다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-policy-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-policy-f04
|
||||
file: ../../../final/evidence/rendered/messaging-policy-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-policy-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-policy.md#L808 이다.
|
||||
module: messaging-policy
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# DLQ 메타데이터의 두 시각이 항상 같다
|
||||
|
||||
DeadLetterMetadata가 firstFailureAt과 lastFailureAt을 별도 필드로 선언하는데, 유일한 생산 지점인 DeadLetterOrchestrator:89-97이 둘 다 delivery.metadata().receivedAt()으로 채운다. 두 헤더(msg.first-failure-at, msg.last-failure-at)가 DLQ 메시지에 붙는데 항상 같은 값이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **구성 오류는 한 예외 타입과 안정 코드로 보고한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **저장소 밖 문서를 절 번호로 인용하지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **부팅 경로의 알고리즘 복잡도는 문서화한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
DeadLetterMetadata가 firstFailureAt과 lastFailureAt을 별도 필드로 선언하는데, 유일한 생산 지점인 DeadLetterOrchestrator:89-97이 둘 다 delivery.metadata().receivedAt()으로 채운다.
|
||||
|
||||
두 헤더(msg.first-failure-at, msg.last-failure-at)가 DLQ 메시지에 붙는데 항상 같은 값이다.
|
||||
|
||||
## 결론
|
||||
|
||||
운영자가 "이 메시지가 얼마나 오래 실패해 왔는가"를 헤더에서 알 수 없다.
|
||||
|
||||
ReservedHeaders가 두 이름을 따로 정의한 목적이 실현되지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : DeadLetterMetadata 참조 5건 검색과 유일한 생산 지점이 두 필드에 넣는 값 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-policy.md#L808 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`DeadLetterMetadata`가 `firstFailureAt`과 `lastFailureAt`을 별도 필드로 선언하는데, 유일한 생산 지점인 `DeadLetterOrchestrator:89-97`이 둘 다 `delivery.metadata().receivedAt()`으로 채운다.
|
||||
|
||||
## DeadLetterMetadata 참조 위치
|
||||
|
||||
:::evidence key="messaging-policy-f04" alt="코드베이스에서 DeadLetterMetadata 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DeadLetterMetadata 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 헤더가 항상 같은 값이다
|
||||
|
||||
`msg.first-failure-at`, `msg.last-failure-at` 가 DLQ 메시지에 붙는다. 운영자가 "이 메시지가 얼마나 오래 실패해 왔는가"를 헤더에서 알 수 없다 — `ReservedHeaders`가 두 이름을 따로 정의한 목적이 실현되지 않는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
DLQ 메시지의 두 헤더가 실제 배포에서 같은 값을 갖는 것을 관측하지 않았다. 생산 지점 한 곳의 코드로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-pulsar-experimental-f01
|
||||
title: 닫힌 전송의 거절이 영구 업무 실패로 분류된다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-pulsar-experimental-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-pulsar-experimental-f01
|
||||
file: ../../../final/evidence/rendered/messaging-pulsar-experimental-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-pulsar-experimental-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-pulsar-experimental.md#L196 이다.
|
||||
module: messaging-pulsar-experimental
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 닫힌 전송의 거절이 영구 업무 실패로 분류된다
|
||||
|
||||
두 호출자가 이 메서드를 쓴다. 첫째는 영구 업무 실패가 맞다.
|
||||
|
||||
## 문제
|
||||
|
||||
두 호출자가 이 메서드를 쓴다.
|
||||
|
||||
첫째는 영구 업무 실패가 맞다.
|
||||
|
||||
## 결론
|
||||
|
||||
둘째는 아니다.
|
||||
|
||||
종료 중이라는 것은 이 세대의 사정이고, 다음 세대나 다른 인스턴스에서는 같은 메시지가 발행된다.
|
||||
|
||||
같은 파일의 classify 가 분류를 신중히 나눈다 — 사전 거절은 CONFIGURATION, 모호는 TRANSIENT_INFRASTRUCTURE.
|
||||
|
||||
닫힘만 그 규율 밖에 있다.
|
||||
|
||||
전송되지 않았다는 증거(notTransmitted)는 옳다.
|
||||
|
||||
어긋난 것은 범주뿐이다.
|
||||
|
||||
수정은 닫힘에 TRANSIENT_INFRASTRUCTURE 를 주거나, 두 호출자가 범주를 인자로 받게 하는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 로컬 거절 헬퍼가 붙이는 범주와 두 호출자의 실패 성격 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-pulsar-experimental.md#L196 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
두 호출자가 로컬 거절 헬퍼를 쓴다. 첫째(적재물 상한 초과)는 영구 업무 실패가 맞다. 둘째(전송 종료 중)는 아니다.
|
||||
|
||||
## 헬퍼를 부르는 두 호출자
|
||||
|
||||
:::evidence key="messaging-pulsar-experimental-f01" alt="분석 문서 analysis/messaging/messaging-pulsar-experimental.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-pulsar-experimental.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 종료 중은 이 세대의 사정이다
|
||||
|
||||
다음 세대나 다른 인스턴스에서는 같은 메시지가 발행된다.
|
||||
|
||||
## 같은 파일의 classify 는 분류를 신중히 나눈다
|
||||
|
||||
사전 거절은 `CONFIGURATION`, 모호는 `TRANSIENT_INFRASTRUCTURE`. 닫힘만 그 규율 밖에 있다.
|
||||
|
||||
## 증거는 옳고 범주만 어긋났다
|
||||
|
||||
전송되지 않았다는 증거(`notTransmitted`)는 옳다. 수정은 닫힘에 `TRANSIENT_INFRASTRUCTURE` 를 주거나, 두 호출자가 범주를 인자로 받게 하는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 Pulsar 브로커를 띄우지 않았다. 이 리프가 클라이언트 브리지를 싣지 않으므로 그럴 대상도 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-rabbit-f01
|
||||
title: 확인 등급이 요구에서 파생되고, 그 요구를 뒷받침하는 강제는 목적지 종류 하나에만 걸린다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-rabbit-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-rabbit-f01
|
||||
file: ../../../final/evidence/rendered/messaging-rabbit-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-rabbit-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-rabbit.md#L210 이다.
|
||||
module: messaging-rabbit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 확인 등급이 요구에서 파생되고, 그 요구를 뒷받침하는 강제는 목적지 종류 하나에만 걸린다
|
||||
|
||||
증거의 등급이 브로커가 무엇을 했는지가 아니라 프로파일이 무엇을 요구했는지 에서 나온다. 대부분의 경우 이 파생은 성립한다.
|
||||
|
||||
## 문제
|
||||
|
||||
증거의 등급이 브로커가 무엇을 했는지가 아니라 프로파일이 무엇을 요구했는지 에서 나온다.
|
||||
|
||||
대부분의 경우 이 파생은 성립한다.
|
||||
|
||||
## 결론
|
||||
|
||||
두 강제가 그것을 받쳐 준다.
|
||||
|
||||
RabbitHeaderMapper.toProperties 가 배달 모드를 무조건 PERSISTENT 로 둔다.
|
||||
|
||||
RabbitMQ 는 지속 메시지를 디스크에 쓴 뒤에 확인한다.
|
||||
|
||||
RabbitProfileValidator.validateDestination 이 내구 작업 큐에 쿼럼 큐를 요구한다.
|
||||
|
||||
쿼럼 큐의 확인은 다수 복제 뒤에 온다.
|
||||
|
||||
빈틈은 둘째 강제의 범위다.
|
||||
|
||||
작업 큐가 아닌 목적지에는 쿼럼 요구가 없다.
|
||||
|
||||
교환기로 발행하는 목적지가 REPLICATION_OR_PERSISTENCE_ACK 를 요구하면, 그 교환기에 바인딩된 큐가 고전 큐여도 어댑터는 그 등급을 보고한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : RabbitHeaderMapper 참조 13건 검색과 확인 등급이 파생되는 입력 및 강제가 걸린 목적지 종류 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-rabbit.md#L210 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
증거의 등급이 브로커가 무엇을 했는지가 아니라 프로파일이 무엇을 **요구했는지** 에서 나온다.
|
||||
|
||||
## RabbitHeaderMapper 참조 위치
|
||||
|
||||
:::evidence key="messaging-rabbit-f01" alt="코드베이스에서 RabbitHeaderMapper 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RabbitHeaderMapper 코드베이스 검색 — 13줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 강제가 그 파생을 받쳐 준다
|
||||
|
||||
`RabbitHeaderMapper.toProperties` 가 배달 모드를 무조건 `PERSISTENT` 로 둔다 — RabbitMQ 는 지속 메시지를 디스크에 쓴 뒤에 확인한다. 그리고 `RabbitProfileValidator.validateDestination` 이 내구 작업 큐에 쿼럼 큐를 요구한다 — 쿼럼 큐의 확인은 다수 복제 뒤에 온다.
|
||||
|
||||
## 빈틈은 둘째 강제의 범위다
|
||||
|
||||
작업 큐가 아닌 목적지에는 쿼럼 요구가 없다. 교환기로 발행하는 목적지가 `REPLICATION_OR_PERSISTENCE_ACK` 를 요구하면, 그 교환기에 바인딩된 큐가 고전 큐여도 어댑터는 그 등급을 보고한다. 지속 모드 덕분에 디스크 기록은 보장되지만 복제는 보장되지 않는다.
|
||||
|
||||
## 이 저장소의 규율과 어긋난다
|
||||
|
||||
증거가 관측에서 나와야 한다는 것이다 — `MessagingCapabilities` 의 javadoc 이 "a silently weakened guarantee is indistinguishable from a working one until the incident" 라고 적는다. 수정은 쿼럼 요구를 목적지 종류가 아니라 **요구된 확인 등급** 에 걸거나, 작업 큐가 아닌 목적지에서는 등급을 `BROKER_ACK` 로 낮추는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 브로커로 반환-먼저-확인 순서를 재현하지 않았다. 그 레인은 컨테이너가 필요하다.
|
||||
|
||||
<!-- body:end -->
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-rabbit-f03
|
||||
title: SCRAM 자격을 RabbitMQ 의 데모 기구로 조용히 매핑한다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-rabbit-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-rabbit-f03
|
||||
file: ../../../final/evidence/rendered/messaging-rabbit-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-rabbit-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-rabbit.md#L274 이다.
|
||||
module: messaging-rabbit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# SCRAM 자격을 RabbitMQ 의 데모 기구로 조용히 매핑한다
|
||||
|
||||
RABBIT-CR-DEMO 는 RabbitMQ 의 시연용 challenge-response 인증 기구(rabbit_auth_mechanism_cr_demo)의 이름이고 기본 활성이 아니다. RabbitMQ 는 SCRAM-SHA 를 구현하지 않으므로 SaslScram 에 대응하는 AMQP 기구가 없다는 것 자체는 사실이다.
|
||||
|
||||
## 문제
|
||||
|
||||
RABBIT-CR-DEMO 는 RabbitMQ 의 시연용 challenge-response 인증 기구(rabbit_auth_mechanism_cr_demo)의 이름이고 기본 활성이 아니다.
|
||||
|
||||
RabbitMQ 는 SCRAM-SHA 를 구현하지 않으므로 SaslScram 에 대응하는 AMQP 기구가 없다는 것 자체는 사실이다.
|
||||
|
||||
## 결론
|
||||
|
||||
문제는 그 사실을 다루는 방식이 같은 파일 안에서 일관되지 않다는 것이다.
|
||||
|
||||
그리고 이웃 어댑터의 같은 클래스가 정확히 이 상황에 대한 규범을 적어 두었다.
|
||||
|
||||
SaslScram 에도 그 규범이 적용되어야 한다.
|
||||
|
||||
플러그인이 없는 브로커에서는 handshake 가 알아보기 어려운 오류로 실패하고, 있는 브로커에서는 시연용 기구로 인증한다.
|
||||
|
||||
수정은 Nkey 와 같이 거부하거나, PLAIN 으로 매핑하고 그 이유를 주석으로 남기는 것이다.
|
||||
|
||||
어느 쪽이든 지금처럼 말없이 데모 기구를 고르는 것보다 낫다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : SaslScram 참조 18건 검색과 매핑되는 기구 이름의 RabbitMQ 기본 활성 여부 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-rabbit.md#L274 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`RABBIT-CR-DEMO` 는 RabbitMQ 의 시연용 challenge-response 인증 기구(`rabbit_auth_mechanism_cr_demo`)의 이름이고 기본 활성이 아니다.
|
||||
|
||||
## SaslScram 참조 위치
|
||||
|
||||
:::evidence key="messaging-rabbit-f03" alt="코드베이스에서 SaslScram 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SaslScram 코드베이스 검색 — 18줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 대응 기구가 없다는 것 자체는 사실이다
|
||||
|
||||
RabbitMQ 는 SCRAM-SHA 를 구현하지 않는다. 문제는 그 사실을 다루는 방식이 같은 파일 안에서 일관되지 않다는 것이다 — 이웃 어댑터의 같은 클래스가 정확히 이 상황에 대한 규범을 적어 두었고, `SaslScram` 에도 그 규범이 적용되어야 한다.
|
||||
|
||||
## 두 브로커 형상에서 결과가 다르다
|
||||
|
||||
플러그인이 없는 브로커에서는 handshake 가 알아보기 어려운 오류로 실패하고, 있는 브로커에서는 시연용 기구로 인증한다. 수정은 `Nkey` 와 같이 거부하거나, `PLAIN` 으로 매핑하고 그 이유를 주석으로 남기는 것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이 기구를 실제 브로커에 붙여 보지 않았다. 이름과 RabbitMQ 의 기본 활성 상태로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-rabbit-f05
|
||||
title: pause 의 의미가 SPI 하나 뒤에서 두 브로커에 다르게 구현된다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-rabbit-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-rabbit-f05
|
||||
file: ../../../final/evidence/rendered/messaging-rabbit-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-rabbit-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-rabbit.md#L335 이다.
|
||||
module: messaging-rabbit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# pause 의 의미가 SPI 하나 뒤에서 두 브로커에 다르게 구현된다
|
||||
|
||||
호출자가 이 단계를 기다리고 나면 "일시정지되었다" 고 읽는다. 실제로 일어난 것은 onMessage 가 이후 배달에 false 를 답하기 시작한 것뿐이고, 리스너 컨테이너는 계속 배달을 밀며 그 배달들은 미확인 상태로 재배달된다.
|
||||
|
||||
## 문제
|
||||
|
||||
호출자가 이 단계를 기다리고 나면 "일시정지되었다" 고 읽는다.
|
||||
|
||||
실제로 일어난 것은 onMessage 가 이후 배달에 false 를 답하기 시작한 것뿐이고, 리스너 컨테이너는 계속 배달을 밀며 그 배달들은 미확인 상태로 재배달된다.
|
||||
|
||||
## 결론
|
||||
|
||||
즉 정지가 아니라 거부-재배달 루프다.
|
||||
|
||||
Kafka 쪽은 같은 SPI 를 정반대로 구현하고 그 이유를 적는다.
|
||||
|
||||
AMQP 에는 대응하는 수단이 있다 — basicCancel 로 소비자를 취소하거나 컨테이너를 멈추는 것.
|
||||
|
||||
지금 구현이 그것을 하지 않는 이유는 어디에도 없다.
|
||||
|
||||
전용 시험(aPausedQueueRefusesDeliveriesSoTheBrokerRedeliversThem)의 이름이 이미 실제 동작을 정확히 말한다.
|
||||
|
||||
그러므로 수정은 둘 중 하나다 — 컨테이너를 실제로 멈추거나, SPI 의 javadoc 에 "브로커에 따라 정지가 거부-재배달일 수 있다" 를 명시하는 것.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 두 브로커의 pause 구현 본문 대조와 리스너 컨테이너의 배달 지속 여부 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-rabbit.md#L335 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
호출자가 pause 단계를 기다리고 나면 "일시정지되었다" 고 읽는다. 실제로 일어난 것은 `onMessage` 가 이후 배달에 `false` 를 답하기 시작한 것뿐이고, 리스너 컨테이너는 계속 배달을 밀며 그 배달들은 미확인 상태로 재배달된다 — 정지가 아니라 거부-재배달 루프다.
|
||||
|
||||
## pause 이후 실제로 일어나는 것
|
||||
|
||||
:::evidence key="messaging-rabbit-f05" alt="분석 문서 analysis/messaging/messaging-rabbit.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-rabbit.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## Kafka 쪽은 같은 SPI 를 정반대로 구현한다
|
||||
|
||||
그리고 그 이유를 적는다. AMQP 에는 대응하는 수단이 있다 — `basicCancel` 로 소비자를 취소하거나 컨테이너를 멈추는 것. 지금 구현이 그것을 하지 않는 이유는 어디에도 없다.
|
||||
|
||||
## 시험 이름이 이미 실제 동작을 말한다
|
||||
|
||||
`aPausedQueueRefusesDeliveriesSoTheBrokerRedeliversThem`. 수정은 둘 중 하나다 — 컨테이너를 실제로 멈추거나, SPI 의 javadoc 에 "브로커에 따라 정지가 거부-재배달일 수 있다" 를 명시하는 것.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실제 브로커로 일시정지 이후의 재배달을 관측하지 않았다. 두 구현의 코드 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-testkit-f03
|
||||
title: Faults 내부클래스 57줄이 3개 모듈에 바이트 단위로 복제되어 있다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-testkit-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-testkit-f03
|
||||
file: ../../../final/evidence/rendered/messaging-testkit-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-testkit-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-testkit.md#L960 이다.
|
||||
module: messaging-testkit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# Faults 내부클래스 57줄이 3개 모듈에 바이트 단위로 복제되어 있다
|
||||
|
||||
sha256 이 세 곳 모두 3028b459… 로 동일하다(EVD-299). 총 171줄.
|
||||
|
||||
## 문제
|
||||
|
||||
sha256 이 세 곳 모두 3028b459… 로 동일하다(EVD-299).
|
||||
|
||||
총 171줄.
|
||||
|
||||
## 결론
|
||||
|
||||
messaging-testkit/src/main 에 DefaultFaultController (또는 RecordingFaultController) 하나를 두고 세 하니스가 그것을 필드로 갖게 하면 된다.
|
||||
|
||||
MessagingAdapterHarness.faults() 의 반환 타입은 FaultController 그대로이므로 외부 API 변경이 없다.
|
||||
|
||||
이 복제가 위험한 이유는 P2 와 겹친다: rejectPublish 의 전송 증거를 고치려면 지금은 세 파일을 고쳐야 하고, 세 파일이 어긋나면 어댑터마다 다른 결함 의미를 갖게 된다 — 이 리프가 존재하는 이유 자체를 무너뜨린다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : Faults 참조 12건 검색과 세 복제본의 sha256 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-testkit.md#L960 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`sha256` 이 세 곳 모두 `3028b459…` 로 동일하다(`EVD-299`). 총 171줄.
|
||||
|
||||
## Faults 참조 위치
|
||||
|
||||
:::evidence key="messaging-testkit-f03" alt="코드베이스에서 Faults 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="Faults 코드베이스 검색 — 12줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 통합 방법
|
||||
|
||||
`messaging-testkit/src/main` 에 `DefaultFaultController`(또는 `RecordingFaultController`) 하나를 두고 세 하니스가 그것을 필드로 갖게 하면 된다. `MessagingAdapterHarness.faults()` 의 반환 타입은 `FaultController` 그대로이므로 외부 API 변경이 없다.
|
||||
|
||||
## 이 복제가 위험한 이유는 P2 와 겹친다
|
||||
|
||||
`rejectPublish` 의 전송 증거를 고치려면 지금은 세 파일을 고쳐야 하고, 세 파일이 어긋나면 어댑터마다 다른 결함 의미를 갖게 된다 — 이 리프가 존재하는 이유 자체를 무너뜨린다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
없음 — 세 사본의 해시가 같다는 것을 직접 확인했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: QUESTION
|
||||
slug: messaging-observability-f03
|
||||
title: 브로커 홉 추적기가 소비자를 갖지 않는다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: open-question:messaging-observability-f03
|
||||
questionStatus: OPEN
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-observability.md#L690
|
||||
---
|
||||
|
||||
# 브로커 홉 추적기가 소비자를 갖지 않는다
|
||||
|
||||
브로커를 사이에 둔 두 span 을 잇는 명시된 수단이 아무 데서도 호출되지 않는다. 어댑터가 각자 하고 있는지, 아무도 하지 않는지가 아직 확인되지 않았다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **타입이 문서화한 불변식은 타입이 강제한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 사실
|
||||
|
||||
MessagingTracer 의 leaf 밖 참조가 0 이다. git grep -n -w MessagingTracer -- src 가 이 leaf 만 돌려준다.
|
||||
|
||||
이 클래스가 존재하는 이유는 "the only way the two spans meet is if the context travels in the message headers" 다.
|
||||
|
||||
messaging-core-api 의 TraceContext 가 봉투 필드로 있고(그쪽 §4.11) 어댑터가 헤더를 매핑한다. 그런데 traceparent · tracestate · baggage 를 헤더로 옮기는 명시된 수단을 아무도 쓰지 않는다.
|
||||
|
||||
## 미지수
|
||||
|
||||
어댑터들이 세 헤더 이름을 각자 다루고 있는가. 그렇다면 MessageHeaders.platform 사용 여부와 빈 추적 처리가 어댑터마다 다를 수 있다.
|
||||
|
||||
## 선택지
|
||||
|
||||
어댑터가 MessagingTracer 를 쓰게 한다
|
||||
세 이름의 처리와 빈 추적 규칙이 한 곳에 모인다.
|
||||
|
||||
어댑터가 대신 하고 있음을 확인하고 이 클래스를 정리한다
|
||||
확인이 먼저다. 중복이 아니라 부재일 수 있다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
messaging-kafka 와 messaging-rabbit 의 헤더 매퍼가 세 이름을 어떻게 다루는지 확인한다. 판정이 그 두 leaf 의 사실에 걸린다.
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
---
|
||||
kind: QUESTION
|
||||
slug: messaging-spring-cloud-stream-bridge-f02
|
||||
title: 브리지의 바인더 쪽 절반이 없다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: open-question:messaging-spring-cloud-stream-bridge-f02
|
||||
questionStatus: OPEN
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-spring-cloud-stream-bridge.md#L561
|
||||
---
|
||||
|
||||
# 브리지의 바인더 쪽 절반이 없다
|
||||
|
||||
정책 · 검증 · 정직성 세 층이 완성돼 있고 그것을 실제 바인딩에 연결하는 코드가 없다. leaf 이름이 약속하는 것의 절반만 존재한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **등록을 받는 컴포넌트는 해제도 제공한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **함께 읽히는 두 맵은 한 값으로 묶는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 사실
|
||||
|
||||
ChannelSend 와 BridgedHandler 두 함수형 인터페이스가 바인더 접촉면이고, 그 구현이 저장소에 없다. git grep -n 'ChannelSend\|BridgedHandler' -- src 가 이 leaf 와 그 테스트만 돌려준다.
|
||||
|
||||
MessagingBindingBridge javadoc 은 존재 이유로 "a service already has Stream bindings and needs to reach the same destinations without a rewrite" 를 든다.
|
||||
|
||||
runtime_memberships: [] 와 정합하므로 오늘의 결함은 아니다.
|
||||
|
||||
## 미지수
|
||||
|
||||
이 leaf 를 완성할 것인가. messaging-kafka-share-experimental §17 첫 항목과 같은 질문이다.
|
||||
|
||||
## 선택지
|
||||
|
||||
바인더 어댑터를 만든다
|
||||
두 인터페이스에 구현이 생기고 leaf 이름이 약속하는 경로가 닫힌다.
|
||||
|
||||
파생 프로젝트의 구현점임을 명시한다
|
||||
구현 부재가 미완이 아니라 설계임을 javadoc 이 말한다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
evidence/raw/296 §A · §B 로 구현 부재는 확정된다. 완성 여부의 결정은 저장소 안의 사실로 닫히지 않는다.
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-kafka-share-experimental-f06
|
||||
title: 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-kafka-share-experimental-f06
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-kafka-share-experimental.md#L512
|
||||
---
|
||||
|
||||
# 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다
|
||||
|
||||
## 관계
|
||||
|
||||
- **"등록"이 아무것도 등록하지 않고 성공을 반환한다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
운영자는 에러 메시지가 시키는 대로 프로퍼티를 설정하고, 아무 일도 일어나지 않는 것을 본다. 그때 의심할 대상이 자기 오타인지 코드인지 판단할 근거가 없다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 에러 메시지에 등장하는 프로퍼티 키를 저장소에서 검색한다
|
||||
KAFKA_SHARE_DISABLED 메시지가 backend.messaging.experimental.kafka-share=true 를 지시한다. git grep -n 'kafka-share' -- src 는 이 leaf 의 문자열 하나만 돌려준다.
|
||||
|
||||
2. 그 키를 읽는 바인딩이 있는지 확인한다
|
||||
enabled 는 KafkaShareProfile 생성자 인자이고, 그 profile 을 만드는 production 코드가 없다.
|
||||
|
||||
3. 바인딩이 없으면 메시지에서 키를 빼거나 바인딩을 함께 만든다
|
||||
지시가 실행 가능해야 지시다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
실패 메시지가 복구 방법을 문장으로 제시하는 모든 자리.
|
||||
|
||||
## 예외
|
||||
|
||||
SSOT 가 이 규칙의 반례를 적지 않았다. 배선 계획이 확정된 키를 미리 안내하는 경우가 있다면 그 사실이 메시지 자체에 있어야 한다.
|
||||
|
||||
## 예시
|
||||
|
||||
git grep -n 'kafka-share' -- src 의 결과가 문자열 하나라는 것과, KafkaShareProfile 을 만드는 production 코드가 없다는 것.
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-runtime-core-f04
|
||||
title: 안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-runtime-core-f04
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-runtime-core.md#L736
|
||||
---
|
||||
|
||||
# 안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다
|
||||
|
||||
## 관계
|
||||
|
||||
- **관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **소비 오케스트레이터가 조립되지 않는다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
PUBLISH_DEADLINE_EXCEEDED 하나가 두 completion 에 붙는데, 두 경우의 운영자 행동은 정반대다. 전송 전이면 버려도 안전하고, 전송 후면 같은 messageId 로만 재발행해야 한다. 대시보드가 코드로 집계하는 순간 그 구분이 사라진다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 코드 하나가 몇 개의 completion 에 붙는지 센다
|
||||
전송 전이면 REJECTED(:16-21), 전송 후면 AMBIGUOUS(:42-47) 다.
|
||||
|
||||
2. 각 completion 에서 운영자가 할 일이 같은지 묻는다
|
||||
다르면 코드가 갈라져야 한다.
|
||||
|
||||
3. FailureDescriptor.code 의 계약을 기준으로 판정한다
|
||||
그 필드는 "stable, machine-readable code" 이고, completion 을 함께 읽어야만 뜻이 정해지는 코드는 그 계약을 지키지 못한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
실패를 안정 코드로 분류하고 그 코드가 대시보드·알람의 집계 키가 되는 모든 자리.
|
||||
|
||||
## 예외
|
||||
|
||||
SSOT 가 이 규칙의 반례를 적지 않았다. 코드를 합치고 completion 을 항상 함께 노출하는 설계가 있다면 그 규약이 어딘가에 선언돼야 한다.
|
||||
|
||||
## 예시
|
||||
|
||||
PUBLISH_DEADLINE_EXCEEDED 가 붙는 두 위치. 확인 방법은 git grep -n 'PUBLISH_DEADLINE_EXCEEDED' -- src/messaging/messaging-runtime-core 다.
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-schema-json-f02
|
||||
title: 실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-schema-json-f02
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-schema-json.md#L472
|
||||
---
|
||||
|
||||
# 실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다
|
||||
|
||||
## 관계
|
||||
|
||||
- **포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
여섯 갈래 중 셋(중복 키 · trailing token · 깊이 초과)은 적대적 입력의 신호이고 나머지 셋은 계약 불일치다. 하나의 코드로 접히면 DLQ 를 보는 운영자가 그 둘을 구분할 수 없다. FailureDescriptor.exceptionType 마저 비어 있어 원인 클래스도 남지 않는다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 한 catch 로 모이는 실패의 종류를 센다
|
||||
decode 의 catch (JacksonException) 단일 분기가 깊이 초과 · 중복 키 · trailing token · 미지 필드 · 문서 길이 · 토큰 길이를 전부 JSON_DECODE_FAILED 로 만든다.
|
||||
|
||||
2. 각각에 대해 운영자가 할 일이 같은지 묻는다
|
||||
공격 신호는 차단으로, 계약 불일치는 스키마 수정으로 이어진다.
|
||||
|
||||
3. 코드를 나누거나 최소한 원인 타입을 채운다
|
||||
JacksonException 하위 타입별로 코드를 나누거나, exceptionType 에 원인 클래스 단순명을 넣는다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
라이브러리 예외 하나를 잡아 자체 실패 코드로 옮기는 모든 디코더·파서 경계.
|
||||
|
||||
## 예외
|
||||
|
||||
SSOT 가 이 규칙의 반례를 적지 않았다. 여섯 갈래의 대응이 실제로 동일하다면 하나의 코드가 맞지만, 여기서는 셋이 보안 신호다.
|
||||
|
||||
## 예시
|
||||
|
||||
JacksonMessageCodec.java:155-158 의 단일 catch. 확인 방법은 JacksonMessageCodecTest 의 네 케이스가 전부 같은 예외 타입을 기대한다는 것이다.
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-transport-spi-f02
|
||||
title: 멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-transport-spi-f02
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-transport-spi.md#L668
|
||||
---
|
||||
|
||||
# 멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다
|
||||
|
||||
## 관계
|
||||
|
||||
- **드레인 마감 30초가 세 곳에서 독립적으로 결정된다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
이 클래스의 다른 모든 경로가 "정확히 한 번 close" 를 CAS 로 보장한다. 이 한 창만 그 보장 밖이고, 남는 것은 닫히지 않은 브로커 연결이다 — §13 의 세 번째 결함과 같은 결과다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 종료 메서드가 상태를 비우는 순서를 본다
|
||||
close() 가 draining 은 synchronized 로 비우고, current 는 List.copyOf(current.keySet()) 후 개별 remove 한다.
|
||||
|
||||
2. 그 사이에 등록이 들어올 수 있는지 본다
|
||||
install 이 새 세대를 넣으면 그 세대는 닫히지 않는다.
|
||||
|
||||
3. 종료 이후의 등록 동작을 정의한다
|
||||
close() 에 종료 플래그를 두고, 그 이후의 install 은 즉시 runtime.close() 하게 한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
세대 교체나 참조 계수로 자원 종료를 보장하면서 등록 API 를 함께 노출하는 registry.
|
||||
|
||||
## 예외
|
||||
|
||||
종료 중 등록이 타입으로 불가능하면 대상이 아니다. 여기서는 install 이 종료 여부를 보지 않는다.
|
||||
|
||||
## 예시
|
||||
|
||||
DefaultMessagingRuntimeRegistry.java:141-156. 확인 방법은 코드 검토이고, 테스트로 재현하려면 close() 중 install 을 끼워 넣어야 한다.
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-transport-spi-f03
|
||||
title: 같은 개념의 sentinel은 계층을 넘어 하나로 정한다
|
||||
topic: transport-and-provider-semantics
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-transport-spi-f03
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-transport-spi.md#L677
|
||||
---
|
||||
|
||||
# 같은 개념의 sentinel은 계층을 넘어 하나로 정한다
|
||||
|
||||
## 관계
|
||||
|
||||
- **드레인 마감 30초가 세 곳에서 독립적으로 결정된다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
PauseResumeController 가 소비자 0 이라 오늘 충돌하지 않는다. 그것을 배선하는 사람이 변환을 넣어야 하고, 빠뜨리면 "" 가 이름이 "" 인 파티션을 가리킨다 — 실패하지 않고 아무것도 일시정지하지 않는다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 같은 개념의 전체 지정자가 계층마다 무엇인지 모은다
|
||||
TransportConsumerRegistration.pause 는 빈 문자열이 전체, PauseResumeController.pause 는 "*" 가 전체다.
|
||||
|
||||
2. 변환 지점이 코드에 있는지 확인한다
|
||||
지금은 두 계층을 잇는 코드 자체가 없다.
|
||||
|
||||
3. sentinel 을 통일하거나 타입으로 바꾼다
|
||||
Optional<String> 이면 sentinel 자체가 사라진다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
"전체" · "없음" · "기본" 같은 특수 의미를 문자열 값에 실어 계층을 넘기는 모든 API.
|
||||
|
||||
## 예외
|
||||
|
||||
두 계층이 절대 만나지 않는다면 대상이 아니다. 이 둘은 만나도록 설계된 상하 계층이다.
|
||||
|
||||
## 예시
|
||||
|
||||
두 javadoc 의 sentinel 정의. 확인 방법은 그 둘을 대조하는 것이다.
|
||||
|
||||
Reference in New Issue
Block a user