Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c03.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

73 lines
4.1 KiB
Markdown

---
kind: CONCEPT
slug: messaging-observability-c03
title: 차원이 닫혀 있고, 사전 확인과 커밋 사이에 lock이 없다
topic: delivery-and-settlement-models
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: concept:messaging-observability-c03
evidenceCapturedOn: 2026-09-01
assets:
- key: messaging-observability-c03
file: ../../../final/evidence/rendered/messaging-observability-c03.svg
- key: messaging-observability-c03-diagram
file: ../../../final/assets/diagrams/messaging-observability-c03.svg
evidence:
- ../../../final/evidence/raw/messaging-observability-c03.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-observability.md#L113 이다.
module: messaging-observability
---
# 차원이 닫혀 있고, 사전 확인과 커밋 사이에 lock이 없다
여섯 차원: broker, destinationProfile, operation, outcome, failureCategory, retryStage. 없는 값은 NONE = "none"이다 — null도 빈 문자열도 아니고 명시적 sentinel이다.
## 관계
- **타입이 문서화한 불변식은 타입이 강제한다**
같은 분석 리프에서 끌어낸 규칙이다.
## 본문
<!-- body:start -->
`MessagingTags`는 열린 map이 아니라 고정 record다 — "Every field here is bounded by configuration or by an enum, so the cardinality of the metric is known before it is ever scraped. Message ids, partition keys, tenant ids, and offsets are all deliberately absent: each of them is unbounded at runtime and would multiply every series by the message volume."
## 태그에서 일부러 뺀 것
:::evidence key="messaging-observability-c03-diagram" alt="여섯 고정 차원과 없는 값은 none 이 MessagingTags 안에 놓이고 message id 와 partition key, tenant id 와 offset 이 바깥에 빗금으로 놓인다" caption="태그에서 일부러 뺀 것" zoom="false"
:::
여섯 차원은 `broker`, `destinationProfile`, `operation`, `outcome`, `failureCategory`, `retryStage`다. 없는 값은 `NONE = "none"`이다 — null도 빈 문자열도 아니고 명시적 sentinel이다.
## 두 factory의 차이
`new MessagingTags(6개 인자)``failureCategory``retryStage`를 호출자가 지정하고, `MessagingTags.of(4개 인자)`는 둘 다 `NONE` 고정이다. 이 차이가 §12.1의 핵심이 된다.
## LinkedHashMap 참조 위치
:::evidence key="messaging-observability-c03" alt="코드베이스에서 LinkedHashMap 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LinkedHashMap 코드베이스 검색 — 10줄 · exit 0" zoom="true"
:::
`asMap()``LinkedHashMap`으로 순서를 고정하고 `Map.copyOf`로 불변화한다.
## 호출자가 철자를 정하지 않는다
`MessagingObservation`은 네 메서드와 네 상수(`PUBLISH`, `CONSUME`, `SETTLE`, `DEAD_LETTER`)를 갖는다. `publish(...)``PublishCompletion``Optional<FailureCategory>`를 받아 **enum에서 문자열을 파생**한다. 이 클래스는 소비자가 0이다(§12.1).
## 이전 결함 둘이 코드에 남아 있다
기본 상한은 200/차원이다. 먼저 lock 없이 `values.contains(value)`로 빠른 경로를 두고, 새 값일 때만 `synchronized`로 들어가 다시 확인한다 — double-checked 패턴이다. 테스트가 경합을 직접 재현한다(`MessagingSecretLeakTest.concurrentAdmissionNeverExceedsTheLimit`).
## 사전 확인과 커밋 사이에 lock이 없다
`wouldAdmit`으로 전수 사전 확인 후 `admit`으로 커밋한다. 그 사이에 lock이 없으므로 두 스레드가 동시에 통과할 수 있고, 그 경우 두 번째 `admit`이 false를 반환해 `admitted &= ...`가 false가 된다 — 상한은 지켜지고 결과만 거절이 된다. 안전한 방향이다.
## 같은 문제를 두 강도로 푼다
거부 목록은 27개 키이고 **두 범주**를 섞어 담는다. `isDenied`가 소문자 정규화 후 정확 일치다. **`messaging-core-api``MessageHeaders.carriesACredential`은 세그먼트 매칭 + 인접 결합**(그쪽 §4.6)인데 이쪽은 정확 일치다(§12.3). `msg.id`가 목록에 리터럴로 들어 있다.
<!-- body:end -->