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
@@ -0,0 +1,715 @@
|
||||
# adapter-inbound-websocket — 코드베이스 분석
|
||||
|
||||
|
||||
## SSOT identity — 2026-08-31 재검증
|
||||
|
||||
- registered leaf id: `adapter-inbound-websocket`
|
||||
- canonical state `analysisFile`: `analysis/17-adapter-inbound-websocket.md` (이 문서) — 이 leaf의 단일 SSOT
|
||||
- source path: `src/adapter/inbound/websocket` · Gradle `:adapter:inbound:websocket`
|
||||
- registry `allowed_dependencies`: `["domain-core", "application-core", "shared-contract"]`
|
||||
- registry `runtime_memberships`: **`[]`**
|
||||
- coverage ledger: `FULL_READ` **253** / `STRUCTURAL_ONLY` **0** / `EXCLUDED` **0** / `UNCLASSIFIED` **0**
|
||||
- 최초 분석 revision `a24ece9c` → 재검증 revision `21234e38` · 이 리프의 변경 파일 **0**
|
||||
- 재검증 증거: `EVD-333`(소스 드리프트 0), `EVD-334`(lane 재실행)
|
||||
|
||||
> 재검증이 확인한 것은 대상이 움직이지 않았다는 사실이지, 아래 서술이 옳다는 보증이 아니다.
|
||||
> 이번 사이클에서 코드에 대고 다시 확인한 항목은 이 문서의 검증 절과 위 증거가 가리키는 범위다.
|
||||
|
||||
---
|
||||
> **분석 대상** `src/adapter/inbound/websocket` · revision `a24ece9cf797f7ea647e33bf846b115208ed1ba5`
|
||||
> **분모** 253 tracked files (main 169 · test 67 · testkit 7 · nginxWebSocketTest 4 · jettyWebSocketTest 1 · brokerRelayTest 1 · governance 4)
|
||||
> **LOC** main Java 12,784 · test Java 9,435
|
||||
> **근거** `evidence/raw/218-inbound-websocket-module-inventory.txt`, 이하 `219`–`226`
|
||||
|
||||
## 0. 이 모듈의 형태 — 하나의 leaf, 세 개의 설정 네임스페이스
|
||||
|
||||
여섯 소스셋(inbound-web과 같은 형태)이고 `META-INF` 자동설정 리소스가 **없다**. 조립은 전적으로 컴포넌트 스캔에 달려 있으며, 컴포지션 루트는 이 leaf를 스캔에서 제외하지 **않는다**(graphql과 반대).
|
||||
|
||||
그런데 스캔이 잡을 수 있는 Spring 애노테이션을 가진 파일이 **169개 중 7개**다:
|
||||
|
||||
```
|
||||
config/WebSocketPlatformSettings.java @ConfigurationProperties(prefix = "backend.websocket")
|
||||
stomp/WebSocketProperties.java @ConfigurationProperties(prefix = "ca-skeleton.websocket")
|
||||
stomp/WebSocketConfig.java @Configuration + @ConditionalOnProperty("ca-skeleton.websocket.enabled")
|
||||
advanced/sockjs/SockJsConfiguration.java @Configuration + prefix "app.websocket-platform.advanced.sockjs"
|
||||
advanced/stomp/StompConfiguration.java @Configuration + prefix "app.websocket-platform.advanced.stomp"
|
||||
advanced/stomp/StompDefaultsConfiguration.java @Configuration + prefix "app.websocket-platform.advanced.stomp"
|
||||
advanced/stomp/rabbit/RabbitBrokerRelayConfiguration.java prefix "app.websocket-platform.advanced.stomp.relay"
|
||||
advanced/stomp/StompBrokerExclusivity.java
|
||||
```
|
||||
|
||||
**세 개의 설정 접두사가 있고 그중 하나에는 소비자가 없다:**
|
||||
|
||||
| 접두사 | 설정 타입 | `@Configuration` 소비자 | yaml 등장 |
|
||||
|---|---|---|---|
|
||||
| `ca-skeleton.websocket.*` | `stomp/WebSocketProperties` | `stomp/WebSocketConfig` | 없음 |
|
||||
| `app.websocket-platform.advanced.*` | (직접 `@ConditionalOnProperty`) | 4개 | 없음 |
|
||||
| **`backend.websocket.*`** | `config/WebSocketPlatformSettings` | **0** | 없음 |
|
||||
|
||||
세 번째가 이 모듈의 핵심 사실이다. `backend.websocket` 네임스페이스가 규정하는 "플랫폼"이 main 169 파일 중 약 90개를 차지하고, 그것을 조립하는 `@Configuration`이 하나도 없다.
|
||||
|
||||
## 1. 커버리지 원장
|
||||
|
||||
| # | sub-scope | main | test | 기타 | 합 | 상태 |
|
||||
|---|---|---:|---:|---:|---:|---|
|
||||
| 1 | governance + `config` + `moduleboundary` + `core` + `evidence` | 24 | 9 | 4 | 37 | **COMPLETE** |
|
||||
| 2 | `protocol` + `codec` + `handshake` + `servlet` + `webflux` | 23 | 6 | – | 29 | **COMPLETE** |
|
||||
| 3 | `handler` + `inbound` + `outbound` + `session` + `lifecycle` + `ordering` | 21 | 9 | – | 30 | **COMPLETE** |
|
||||
| 4 | `security` + `authz` + `idempotency` + `budget` + `error` + `observability` + `admin` + `release` | 22 | 9 | – | 31 | **COMPLETE** |
|
||||
| 5 | `stomp` | 8 | 5 | – | 13 | **COMPLETE** |
|
||||
| 6 | `advanced/stomp` + `stomp/rabbit` + `cluster` + `resume` | 41 | 13 | – | 54 | **COMPLETE** |
|
||||
| 7 | `advanced/` 잔여 (presence · codec 3종 · sockjs · graphql · compression · client · release · http2 · http3) | 30 | 11 | – | 41 | **COMPLETE** |
|
||||
| 8 | `testkit` + 대체 소스셋 3종 | 0 | 5 | 13 | 18 | **COMPLETE** |
|
||||
| | **TOTAL** | **169** | **67** | **17** | **253** | **8 / 8** |
|
||||
|
||||
`FULL_READ 253 · STRUCTURAL_ONLY 0 · EXCLUDED 0 · UNCLASSIFIED 0`. 분할은 `218-...`의 패키지 트리에서 기계 계산(중복 0 · 미할당 0).
|
||||
|
||||
> **evidence 파일의 `autoconf` 열에 대하여** — `219`–`226`의 배선 표는 graphql 모듈에서 쓴 스크립트를 재사용했고, 그 열은 `autoconfigure` 패키지의 참조 수를 센다. 이 leaf에는 그런 패키지가 없으므로 **전 행이 0이며 그 자체로는 정보가 없다**. 이 모듈의 배선 판정은 위 §0의 애노테이션·네임스페이스 전수와 `main_other` 열로 한다.
|
||||
|
||||
---
|
||||
|
||||
# Sub-scope 01 — governance + `config` + `moduleboundary` + `core` + `evidence` (37 files)
|
||||
|
||||
> 근거 `evidence/raw/219-inbound-websocket-core-probes.txt`
|
||||
|
||||
## 2. 무엇을 하는 코드인가
|
||||
|
||||
**`config` (3)** 이 플랫폼의 안전 장치 셋을 담는다.
|
||||
|
||||
`WebSocketPlatformSettings`(60)는 기본값 철학을 먼저 말한다:
|
||||
|
||||
> "Every default here is the safe one, and where there is no safe default there is no default. `allowedOrigins` is empty and `enabled` is false: **a deployment that has not said which origins may connect has not configured a WebSocket endpoint, and starting one anyway would mean the platform chose its own CSRF posture.**"
|
||||
|
||||
`WebSocketPlatformStartupValidator`(125)는 "개발에서는 완벽히 동작하고 프로덕션에서 틀린" 것만 잡는다고 선언한다:
|
||||
|
||||
> "a cookie endpoint with no origin allowlist serves every same-origin test correctly, an unnegotiated-fallback profile connects fine until the message format changes, and an endpoint with no authorization requirement simply refuses everything — quietly, and only for the clients that try to use it."
|
||||
|
||||
`WebSocketStackExclusivity`(78)는 **inbound-web §40.1에서 확인한 결함을 이름 붙여 탐지한다**:
|
||||
|
||||
> "Spring Boot deduces one application type from what is present, and the deduction is not negotiable: with both a servlet container and Reactor Netty available it starts the servlet one. **A deployment that declared reactive endpoints and shipped both therefore starts, reports itself healthy, and serves none of them — no error, no warning, and the endpoints simply never answer.**"
|
||||
|
||||
**`core` (12)** 는 값 어휘다 — `WebSocketActorReference`(main_other=19) · `WebSocketEndpointName`(17) · `WebSocketConnectionId`(9) · `WebSocketNodeId`(7) · `WebSocketSubprotocolName`(7) · `WebSocketEndpointCatalog`(111) · `WebSocketConnectionContext`(97). 이 leaf에서 가장 널리 참조되는 타입들이다.
|
||||
|
||||
**`evidence` (6)** 는 연결·인바운드·아웃바운드 3축 증거 모델(`WebSocketInboundEvidence` 121 · `WebSocketOutboundEvidence` 92 · `WebSocketConnectionEvidence` 67).
|
||||
|
||||
**`moduleboundary` (3)** 는 `WebSocketStableModule`(394줄 enum) + `WebSocketModuleBoundary`(98) + `WebSocketModulePurity` — inbound-web의 `WebStableModule`(539)과 같은 형태의 모듈 경계 선언이다.
|
||||
|
||||
## 3. Negative-space probes — sub-scope 01
|
||||
|
||||
### 3.1 (8.1) 도달성 — 세 안전 장치의 호출자
|
||||
|
||||
```
|
||||
$ grep -rn 'WebSocketPlatformStartupValidator|WebSocketStackExclusivity' src/main src/test --include=*.java | grep -v config/
|
||||
advanced/stomp/StompBrokerExclusivity.java:17: * WebSocketStackExclusivity}: a runtime that silently half-applies is worse than one that will not
|
||||
```
|
||||
|
||||
**둘 다 프로덕션 호출자가 0이다.** 유일한 참조는 다른 클래스의 javadoc이 설계를 인용한 것이다. `WebSocketPlatformSettings`도 소비자가 0이다(§0).
|
||||
|
||||
### 3.2 (8.2) 조건 형제 비교 — 두 개의 설정 검증
|
||||
|
||||
| | `stomp/WebSocketProperties` | `config/WebSocketPlatformSettings` |
|
||||
|---|---|---|
|
||||
| 접두사 | `ca-skeleton.websocket` | `backend.websocket` |
|
||||
| 검증 | `@Validated` — "Invalid enabled settings fail context startup"(CLAUDE.md) | `WebSocketPlatformStartupValidator` 125줄 |
|
||||
| 소비 `@Configuration` | `WebSocketConfig` | **없음** |
|
||||
| CLAUDE.md 문서화 | 4개 키 표 | **없음** |
|
||||
|
||||
### 3.3 (8.3) 중복 메커니즘 — origin 허용목록이 두 곳에 있다
|
||||
|
||||
`WebSocketPlatformSettings.allowedOrigins`(기본 빈 집합, "no safe default there is no default")와 `stomp/WebSocketProperties.allowedOriginPatterns()`(기본 `http://localhost:3000`, CLAUDE.md가 "explicit HTTP(S) origins only; blank/wildcard/path rejected"로 규정)이 같은 결정을 두 번 표현한다. 실제 핸드셰이크에 적용되는 것은 후자다(`WebSocketConfig.registerStompEndpoints`).
|
||||
|
||||
### 3.4 (8.4) 문서/구현 드리프트 — CLAUDE.md가 서술하는 모듈과 실제 파일
|
||||
|
||||
CLAUDE.md의 Responsibility 다섯 줄이 전부 STOMP-over-SockJS 어댑터를 서술하고, Typed settings 표는 `ca-skeleton.websocket.*` 네 키만 담는다. `backend.websocket`과 `app.websocket-platform.advanced.*` 두 네임스페이스는 등장하지 않는다.
|
||||
|
||||
Evidence 절이 일부를 명시적으로 면책한다:
|
||||
|
||||
> "The simple broker is local, single-process, best-effort R1 evidence only. **Broker relay, multi-node/durable delivery, rollback-safe publication, replay/resume, backpressure, and a versioned domain projection catalog are P2 and are not claimed.**"
|
||||
|
||||
이 면책이 덮는 것은 `advanced/stomp/rabbit`(7) · `advanced/cluster`(9) · `advanced/resume`(8)과 backpressure 관련 파일이다. 덮지 않는 것이 §4.1이다.
|
||||
|
||||
## 4. Sub-scope 01 findings
|
||||
|
||||
### 4.1 P2 — `backend.websocket` 플랫폼(약 90개 main 파일)에 조립 지점이 없고, 모듈 SSOT 문서에 존재하지 않는다
|
||||
|
||||
169개 main 파일이 세 덩어리로 나뉜다:
|
||||
|
||||
| 덩어리 | 파일 | 조립 | CLAUDE.md |
|
||||
|---|---:|---|---|
|
||||
| STOMP 어댑터 (`stomp/`) | 8 | `WebSocketConfig` (`ca-skeleton.websocket.enabled=true`) | Responsibility 5줄 + 설정 표 4키 + 증거 절 |
|
||||
| Advanced (`advanced/**`) | 71 | 4개 `@Configuration` (`app.websocket-platform.advanced.*`) | 일부 면책("not claimed") |
|
||||
| **플랫폼 (`config`·`core`·`protocol`·`handler`·`outbound`·`session`·`lifecycle`·`ordering`·`security`·`authz`·`idempotency`·`budget`·`error`·`evidence`·`observability`·`admin`·`release`·`handshake`·`codec`·`servlet`·`webflux`·`inbound`·`moduleboundary`)** | **90** | **없음** | **없음** |
|
||||
|
||||
세 번째 덩어리에는 실질적인 기계가 들어 있다 — `PlatformWebSocketHandler`(205) · `OutboundQueue`(189) · `SerializedOutboundWriter`(188) · `WebSocketSessionRegistry`(163) · `WebSocketCorrelationRegistry`(162) · `HandshakeAdmissionPipeline`(145) · `StrictWebSocketJsonCodec`(145) · `CloseOrchestration`(138) · `LateResponseTombstone`(108) · `FragmentAssembler`(117).
|
||||
|
||||
**가장 무거운 결과는 세 안전 장치가 실행되지 않는다는 것이다:**
|
||||
|
||||
1. `WebSocketPlatformStartupValidator`(125) — "Refuses to start a deployment whose WebSocket configuration is unsafe or incoherent." 호출자 0.
|
||||
2. `WebSocketStackExclusivity`(78) — 서블릿/리액티브 이중 스택에서 "reactive endpoints ... simply never answer"를 탐지. 호출자 0.
|
||||
3. `WebSocketPlatformSettings`의 "safe default" 규약 — 그 설정을 읽는 코드가 0.
|
||||
|
||||
**실패 시나리오** — 팀이 이 leaf를 채택하며 `backend.websocket.enabled=true`와 `allowed-origins`를 설정한다. `@ConfigurationPropertiesScan`이 `dev.caskeleton.adapter.inbound.websocket`을 포함하므로 프로퍼티는 바인딩되고 검증도 통과한다(설정 자체는 유효하므로). 부팅이 성공하고 오류가 없다. **WebSocket 엔드포인트는 하나도 열리지 않는다** — `backend.websocket`을 읽는 `@Configuration`이 없기 때문이다. 실제로 엔드포인트를 여는 스위치는 문서화된 `ca-skeleton.websocket.enabled`이고, 그것은 다른 8개 파일짜리 STOMP 어댑터를 켠다.
|
||||
|
||||
**inbound-web·graphql과의 위치** — graphql은 같은 상태를 등급표로 공시했고(§45.3), web은 공시하지 않아 P1 여섯 건이 되었다. 이 모듈은 **일부만 면책한다**(§3.4의 P2 목록) — 그 면책이 `advanced/**`를 덮고 90개 파일의 플랫폼은 덮지 않는다. 그래서 P1이다.
|
||||
|
||||
**권고** — 셋 중 하나. (a) `backend.websocket` 플랫폼을 조립하는 `@Configuration`(또는 `AutoConfiguration.imports` 진입점)을 추가하고 세 안전 장치를 그 안에서 호출한다. (b) 그 플랫폼을 graphql처럼 `modelled` 등급으로 CLAUDE.md에 공시한다. (c) 제거한다. 지금은 셋 다 아니며, 특히 `WebSocketPlatformSettings`가 `@ConfigurationPropertiesScan`에 걸려 **바인딩만 되는** 상태가 (a)를 이미 절반 시사한다.
|
||||
|
||||
### 4.2 P3/기록 — origin 허용목록이 두 네임스페이스에 중복 선언돼 있다
|
||||
|
||||
§3.3. 실제 적용은 `ca-skeleton.websocket.allowed-origins`이고, `backend.websocket.allowed-origins`는 바인딩되지만 읽히지 않는다. 운영자가 후자를 설정하면 "설정했는데 적용되지 않는" 상태가 되고, 두 기본값이 다르므로(빈 집합 대 `http://localhost:3000`) 어느 쪽을 설정했는지에 따라 결과가 정반대다.
|
||||
|
||||
---
|
||||
|
||||
# Sub-scope 02 — `protocol` + `codec` + `handshake` + `servlet` + `webflux` (29 files, main 23 + test 6)
|
||||
|
||||
> 근거 `evidence/raw/220-inbound-websocket-protocol-probes.txt`
|
||||
|
||||
## 5. 무엇을 하는 코드인가
|
||||
|
||||
와이어 계약이다. `WebSocketMessageCatalog`(100) + `WebSocketMessageDescriptor` + `WebSocketMessageType`(main_other=16) + `WebSocketMessageFamily` + `WebSocketSchemaVersion` + `WebSocketEnvelope`(150) + `WebSocketProtocolProfile`(101) + `WebSocketCatalogFingerprint`(64)가 메시지 카탈로그와 봉투를, `StrictWebSocketJsonCodec`(145)이 엄격한 JSON 디코딩을, `WebSocketWireTypeManifest`가 고정 표현을 담는다.
|
||||
|
||||
핸드셰이크는 `HandshakeAdmissionPipeline`(145) + `HandshakeDecision` + `HandshakeRequest`, 전송은 `PlatformWebSocketHandler`(205) + `ServletFrameSink`(45) / `ReactiveFrameSink`(82) + `WebSocketDataBufferLifecycle`(106) + `WebSocketDataBufferPolicy`.
|
||||
|
||||
`ReactiveFrameSink`는 main·test 참조가 **모두 0**인 유일한 파일이다.
|
||||
|
||||
## 6. Negative-space probes
|
||||
|
||||
### 6.1 (8.1) 도달성
|
||||
|
||||
23개 main 파일 중 `@Configuration`/`@Component`가 하나도 없고, 이 leaf의 유일한 `@Configuration`(`stomp/WebSocketConfig`)이 이 패키지들을 참조하지 않는다. §4.1의 90개 플랫폼 파일 중 23개다.
|
||||
|
||||
`PlatformWebSocketHandler`(205)가 이 sub-scope의 중심이고 main 참조 0 · test 1이다 — 이 leaf가 Spring의 `WebSocketHandler`로 등록할 핸들러를 갖고 있으면서 등록하지 않는다.
|
||||
|
||||
### 6.2 (8.2) 조건 형제 비교 — 두 전송의 프레임 싱크
|
||||
|
||||
`ServletFrameSink`(45, main_other=1)와 `ReactiveFrameSink`(82, main_other=0 · test=0). 서블릿 쪽은 최소한 다른 main 파일이 참조하고, 리액티브 쪽은 참조가 없다. §4.1의 `WebSocketStackExclusivity`가 탐지하려던 상황(리액티브 엔드포인트를 선언했는데 서블릿으로 뜨는 배포)에서 실제로 무엇이 죽는지를 이 비대칭이 보여준다.
|
||||
|
||||
### 6.3 (8.3)·(8.4) 중복·드리프트 — 없음
|
||||
|
||||
카탈로그·봉투·코덱이 각각 한 벌이고, `WebSocketCatalogFingerprint`가 카탈로그 변경을 지문으로 고정한다.
|
||||
|
||||
## 7. Findings
|
||||
|
||||
### 7.1 P3/기록 — `ReactiveFrameSink`는 테스트조차 없다
|
||||
|
||||
23개 파일 중 유일하게 main·test 참조가 모두 0이다. 리액티브 전송이 이 leaf에서 도달 불가라는 사실(§6.2)의 가장 뚜렷한 표시다.
|
||||
|
||||
나머지는 §4.1에 포함된다 — 개별 결함이 아니라 90개 플랫폼 파일이 조립되지 않는다는 하나의 사실이다.
|
||||
|
||||
---
|
||||
|
||||
# Sub-scope 03 — `handler` + `inbound` + `outbound` + `session` + `lifecycle` + `ordering` (30 files, main 21 + test 9)
|
||||
|
||||
> 근거 `evidence/raw/221-inbound-websocket-session-probes.txt`
|
||||
|
||||
## 8. 무엇을 하는 코드인가
|
||||
|
||||
세션 수명주기와 아웃바운드 전달의 기계다.
|
||||
|
||||
`OutboundQueue`(189) + `SerializedOutboundWriter`(188) + `OutboundPriority` + `OutboundDelivery` + `OutboundEnqueueResult` + `GlobalBufferBudget`(89) + `OutboundQueueSnapshot`가 우선순위 큐와 직렬 쓰기, 전역 버퍼 예산을 담는다. 백프레셔 모델이다 — CLAUDE.md가 "backpressure ... are P2 and are not claimed"로 면책한 항목 중 하나가 여기 있다.
|
||||
|
||||
`WebSocketSessionRegistry`(163) · `CloseOrchestration`(138) · `HeartbeatPolicy`(65)가 세션 등록과 종료 협상을, `WebSocketCorrelationRegistry`(162) + `LateResponseTombstone`(108)이 요청-응답 상관과 늦은 응답 처리를, `FragmentAssembler`(117)가 프레임 조립을, `ordering` 3종(`GapDetector` 98 · `StreamSequencer` 54 · `OrderingProfile` 54)이 순서 보장을 담당한다.
|
||||
|
||||
## 9. Negative-space probes
|
||||
|
||||
### 9.1 (8.1) 도달성
|
||||
|
||||
21개 main 파일 중 main 참조 0인 것이 여덟이다 — `LateResponseTombstone` · `WebSocketCorrelationRegistry` · `WebSocketMessageHandler`(참조 0·테스트 0) · `WebSocketSessionRegistry` · `CloseOrchestration` · `HeartbeatPolicy` · `GapDetector` · `OrderingProfile` · `StreamSequencer`.
|
||||
|
||||
`OutboundQueue`·`SerializedOutboundWriter`·`GlobalBufferBudget`는 서로를 참조하는 내부 클러스터를 이룬다. 클러스터 전체의 진입점이 `PlatformWebSocketHandler`(§6.1, 미등록)다.
|
||||
|
||||
### 9.2 (8.4) 문서와의 대조
|
||||
|
||||
CLAUDE.md의 면책 목록이 backpressure를 포함하므로 `outbound`(8)는 공시된 범위 안이다. `session`·`lifecycle`·`ordering`·`handler`·`inbound`(13 파일)는 면책 목록에 없다.
|
||||
|
||||
## 10. Findings
|
||||
|
||||
### 10.1 P3/기록 — `WebSocketMessageHandler`는 참조도 테스트도 없다
|
||||
|
||||
39줄 인터페이스이고 구현도 호출자도 없다. `PlatformWebSocketHandler`가 그 자리를 대신하는지는 코드로 판정되지 않는다.
|
||||
|
||||
나머지는 §4.1에 포함된다.
|
||||
|
||||
---
|
||||
|
||||
# Sub-scope 04 — `security` + `authz` + `idempotency` + `budget` + `error` + `observability` + `admin` + `release` (31 files, main 22 + test 9)
|
||||
|
||||
> 근거 `evidence/raw/222-inbound-websocket-policy-probes.txt`
|
||||
|
||||
## 11. 무엇을 하는 코드인가
|
||||
|
||||
플랫폼의 정책 계층. `WebSocketOriginPolicy`(116) · `WebSocketConnectionTicket`(77) · `WebSocketTicketStore` · `WebSocketAuthenticationProfile`(57) · `MessageAuthorizationPolicy`(93) · `WebSocketConnectionBudget`(103, main_other=5) · `WebSocketCloseCode`(95) · `WebSocketClosePolicy`(73) · `WebSocketFailureCategory`(59, main_other=10).
|
||||
|
||||
멱등성은 `CommandReconciliation`(84) + `CommittedResultLedger`(61) + `WebSocketCommandKey` + `WebSocketCommandOutcome`.
|
||||
|
||||
관측은 `SafeWebSocketLogFields`(65) + `WebSocketMetricTags`(76) — 둘 다 main 참조 0.
|
||||
|
||||
`release` 3종은 `WebSocketStableReleaseGate`(106) · `WebSocketRollingRestartScenario`(113) · `WebSocketNginxProxyProfile`(91).
|
||||
|
||||
## 12. Negative-space probes
|
||||
|
||||
### 12.1 (8.1) 도달성 — 정책의 실제 적용 지점
|
||||
|
||||
이 sub-scope에서 실제로 요청 경로에 있는 것은 **`stomp` 패키지가 참조하는 것뿐**이다. `stomp/WebSocketInboundAuthorizationInterceptor`(53)와 `stomp/AuthenticatedHandshakeInterceptor`(34)가 `WebSocketConfig`에 등록되고, 그 둘은 `stomp/WebSocketProperties`를 쓴다.
|
||||
|
||||
`security`(4) · `authz`(1) · `idempotency`(4) · `budget`(1)의 플랫폼 정책 타입은 `stomp`가 참조하지 않는다. 즉 **인증 프로파일 · 티켓 · origin 정책 · 메시지 권한 · 연결 예산 · 명령 멱등성이 모두 요청 경로 밖이다.**
|
||||
|
||||
### 12.2 (8.2) 조건 형제 비교 — 두 개의 인바운드 권한
|
||||
|
||||
| | `stomp/WebSocketInboundAuthorizationInterceptor` (53) | `security/MessageAuthorizationPolicy` (93) |
|
||||
|---|---|---|
|
||||
| 등록 | `WebSocketConfig.configureClientInboundChannel` | 없음 |
|
||||
| 근거 | `stomp/WebSocketProperties` | 플랫폼 정책 모델 |
|
||||
| 범위 | STOMP 목적지 권한 | 메시지 종류별 권한 |
|
||||
|
||||
CLAUDE.md의 Inbound policy 절이 전자를 규정한다. 후자는 문서에 없다.
|
||||
|
||||
### 12.3 (8.4) 카운트 — `WebSocketFailureCategory`
|
||||
|
||||
`error` 패키지의 `WebSocketFailureCategory`(main_other=10)가 이 sub-scope에서 가장 널리 참조되는 타입이고, `WebSocketErrorMessage`(75, main 참조 0)와 `WebSocketErrorTransport`가 그것을 전송으로 옮긴다. 실제 STOMP 오류는 `stomp/SafeStompSubProtocolErrorHandler`(32)가 만든다 — 세 번째 오류 형식이다.
|
||||
|
||||
## 13. Findings
|
||||
|
||||
### 13.1 P2 — 연결 티켓·origin 정책·메시지 권한·연결 예산이 요청 경로 밖이고, 그중 일부는 STOMP 어댑터가 다른 방식으로 대체한다
|
||||
|
||||
§12.1·§12.2. 실제 배포에서 적용되는 보안은 `stomp` 패키지의 두 인터셉터이고, 그것은 CLAUDE.md가 서술하는 범위다("HTTP-handshake principal enforcement and client-inbound STOMP destination authorization").
|
||||
|
||||
플랫폼 정책 계층(22 파일)이 그보다 넓은 모델을 담는다 — 연결 티켓(핸드셰이크 전 발급), 인증 프로파일, origin 정책 116줄, 메시지 종류별 권한, 연결당 예산, 명령 멱등성 원장. **어느 것도 적용되지 않는다.**
|
||||
|
||||
노출은 아니다 — 대체 경로가 더 좁을 뿐 존재하며, CLAUDE.md가 그 좁은 범위를 정확히 서술한다. 기록하는 것은 두 보안 모델이 한 leaf에 공존하고 넓은 쪽이 꺼져 있다는 사실이며, §4.1의 부분집합이다.
|
||||
|
||||
### 13.2 P3/기록 — 오류 형식이 셋이다
|
||||
|
||||
`WebSocketErrorMessage`(플랫폼, 미배선) · `SafeStompSubProtocolErrorHandler`(STOMP, 배선) · `WebSocketErrorTransport`(전송 추상, 미배선). 실제 클라이언트가 받는 것은 두 번째 하나다.
|
||||
|
||||
---
|
||||
|
||||
# Sub-scope 05 — `stomp` (13 files, main 8 + test 5)
|
||||
|
||||
> 근거 `evidence/raw/223-inbound-websocket-stomp-probes.txt`
|
||||
|
||||
## 14. 무엇을 하는 코드인가 — 이 모듈에서 실제로 동작하는 부분
|
||||
|
||||
여덟 개 파일이 CLAUDE.md가 서술하는 모듈 전체다.
|
||||
|
||||
```java
|
||||
// stomp/WebSocketConfig.java
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
@EnableConfigurationProperties(WebSocketProperties.class)
|
||||
@ConditionalOnProperty(prefix = "ca-skeleton.websocket", name = "enabled", havingValue = "true")
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
registerStompEndpoints: registry.setErrorHandler(errorHandler);
|
||||
registry.addEndpoint(properties.getEndpoint())
|
||||
.setAllowedOriginPatterns(properties.allowedOriginPatterns())
|
||||
.addInterceptors(handshakeInterceptor)
|
||||
.withSockJS();
|
||||
configureClientInboundChannel: registration.interceptors(inboundAuthorization);
|
||||
configureMessageBroker: registry.enableSimpleBroker("/topic");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
@Bean LiveEventStompBroadcaster(SimpMessagingTemplate, WebSocketProperties, List<LiveEventProjector<?>>)
|
||||
}
|
||||
```
|
||||
|
||||
`LiveEventStompBroadcaster`(139)가 도메인 이벤트를 `LiveEventProjector` 허용목록을 통해서만 밀어낸다 — CLAUDE.md의 Forbidden이 "Raw `@DomainEvent` payload transmission or reflection-based event serialization"을 금지하고, 프로젝터가 `Map<String,String>` 경계 투영만 낸다.
|
||||
|
||||
`WebSocketProperties`(117)가 `@Validated`이고 CLAUDE.md 표의 네 키를 담는다. `AuthenticatedHandshakeInterceptor`(34)가 핸드셰이크에 비어 있지 않은 `Principal`을 요구하고, `WebSocketInboundAuthorizationInterceptor`(53)가 클라이언트 인바운드 채널에서 목적지를 검사하며, `SafeStompSubProtocolErrorHandler`(32)가 고정 ERROR를 낸다.
|
||||
|
||||
## 15. Negative-space probes
|
||||
|
||||
### 15.1 (8.1) 도달성 — 여덟 파일 전부 배선
|
||||
|
||||
`WebSocketConfig`가 나머지 일곱을 직접 생성하거나 `@Bean`으로 만든다. 미도달 파일이 없다.
|
||||
|
||||
### 15.2 (8.2) 조건 형제 비교 — 이 어댑터와 플랫폼
|
||||
|
||||
§12.2·§13.1. 같은 결정(핸드셰이크 인증 · 인바운드 권한 · 오류 형식 · origin)에 대해 두 구현이 있고 이쪽만 배선된다.
|
||||
|
||||
### 15.3 (8.4) 문서 일치
|
||||
|
||||
CLAUDE.md의 Responsibility 다섯 줄 · Typed settings 네 키 · Inbound policy · Event projection contract · Evidence 절이 이 여덟 파일과 정확히 대응한다. 드리프트 없음.
|
||||
|
||||
## 16. Findings — 없음
|
||||
|
||||
이 sub-scope는 문서·구현·테스트가 일치한다. `WebSocketBoundaryQualificationTest`가 랜덤 포트 Tomcat에서 실제 SockJS/STOMP 핸드셰이크 · Origin · principal · 구독 · 서버 투영 push · 애플리케이션 SEND · 브로커 SEND 거부 · ERROR 리댁션을 교차 확인한다.
|
||||
|
||||
---
|
||||
|
||||
# Sub-scope 06 — `advanced/stomp` + `stomp/rabbit` + `cluster` + `resume` (54 files, main 41 + test 13)
|
||||
|
||||
> 근거 `evidence/raw/224-inbound-websocket-advanced-stomp-probes.txt`
|
||||
|
||||
## 17. 무엇을 하는 코드인가
|
||||
|
||||
**`advanced/stomp` (17)** 은 STOMP 정책 계층이다 — `StompProfile`(118, main_other=4) · `StompDestinationCatalog`(129) · `StompAuthorizationPolicy`(93) · `StompSecurityInterceptor`(96) · `StompAckPolicy`(79) · `StompAckMode`(76) · `StompBrokerExclusivity`(74) · `StompRefusal` · `StompEvidence`.
|
||||
|
||||
`StompBrokerExclusivity`(74)가 §2의 `WebSocketStackExclusivity`를 인용하며 같은 원칙을 브로커에 적용한다 — "a runtime that silently half-applies is worse than one that will not [start]".
|
||||
|
||||
**`advanced/stomp/rabbit` (7)** 은 외부 브로커 릴레이 — `RabbitBrokerRelayConfiguration`(72, `@Configuration`) · `RabbitBrokerRelayProfile`(78) · `MultiNodeUserDestination`(98) · `UserDestinationPolicy`(50) · `UserDestinationRouting`(42) · `UserSessionLocation`.
|
||||
|
||||
**`advanced/cluster` (9)** 는 다중 노드 — `PortBackedExternalSessionIndex`(108) · `MessagingFanoutAdapter`(102) · `FanoutDeduplicator`(100) · `FanoutEnvelope`(75) · `ExternalSessionIndex` · `ExternalSessionSummary`.
|
||||
|
||||
**`advanced/resume` (8)** 은 재개 — `ResumeTokenCodec`(217, 이 sub-scope에서 가장 큰 파일) · `ReplayEventMapper`(132) · `ResumeCoordinator`(111) · `ResumeDecision`(87) · `ResumeTokenKeyRing`(87) · `ResumeTokenPayload`(83) · `ReplayCursor`(64).
|
||||
|
||||
## 18. Negative-space probes
|
||||
|
||||
### 18.1 (8.1) 도달성 — 두 `@Configuration`이 실제로 무엇을 만드는가
|
||||
|
||||
| `@Configuration` | 게이트 | `@Bean` | leaf 내부 import |
|
||||
|---|---|---:|---|
|
||||
| `advanced/stomp/StompConfiguration` (52) | `app.websocket-platform.advanced.stomp` | **0** | 없음 |
|
||||
| `advanced/stomp/StompDefaultsConfiguration` (58) | 같음 | 4 | 없음(프레임워크 빈만) |
|
||||
| `advanced/stomp/rabbit/RabbitBrokerRelayConfiguration` (72) | `...advanced.stomp.relay` | 3 | `advanced.stomp`, `core` |
|
||||
| `advanced/sockjs/SockJsConfiguration` (103) | `...advanced.sockjs` | 1 | `advanced.compression`, `core`, `security` |
|
||||
|
||||
`StompConfiguration`은 `@Bean`이 하나도 없고 leaf 타입을 import하지도 않는다 — 프레임워크 설정만 조정하는 `WebSocketMessageBrokerConfigurer` 계열로 보인다.
|
||||
|
||||
**즉 41개 파일 중 `RabbitBrokerRelayConfiguration`이 참조하는 `advanced.stomp`·`core` 일부만 조립 가능하고, `cluster`(9)와 `resume`(8)은 어떤 `@Configuration`도 참조하지 않는다.**
|
||||
|
||||
### 18.2 (8.4) 문서와의 대조 — 이 sub-scope는 명시적으로 면책돼 있다
|
||||
|
||||
CLAUDE.md Evidence 절:
|
||||
|
||||
> "**Broker relay, multi-node/durable delivery, rollback-safe publication, replay/resume, backpressure, and a versioned domain projection catalog are P2 and are not claimed.**"
|
||||
|
||||
- broker relay → `advanced/stomp/rabbit` (7)
|
||||
- multi-node/durable delivery → `advanced/cluster` (9)
|
||||
- replay/resume → `advanced/resume` (8)
|
||||
|
||||
**24개 파일이 면책 목록에 정확히 대응한다.** `advanced/stomp`(17)는 목록에 없지만 STOMP 정책이므로 "not claimed" 범위로 읽는 것이 자연스럽다.
|
||||
|
||||
### 18.3 (8.2) 조건 형제 비교 — 재개 토큰 서명
|
||||
|
||||
`ResumeTokenCodec`(217) + `ResumeTokenKeyRing`(87)이 서명된 재개 토큰을 만든다. 이 조합은 graphql §24.1의 `HmacGraphQlCursorCodec` + `GraphQlCursorKeyRing`과 같은 형태다. **차이는 이쪽에는 그 키를 요구하는 시작 검증기가 없다는 것** — 즉 "키를 요구하고 서명하지 않는" 잘못된 확인 신호가 없다. 면책 목록에 replay/resume이 있으므로 문서·코드·검증이 일치한다.
|
||||
|
||||
## 19. Findings — 없음
|
||||
|
||||
41개 파일이 CLAUDE.md의 면책 범위 안에 있고, 조립되지 않는다는 사실이 문서와 일치한다. §4.1의 P1은 이 sub-scope를 포함하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
# Sub-scope 07 — `advanced/` 잔여 (41 files, main 30 + test 11)
|
||||
|
||||
> 근거 `evidence/raw/225-inbound-websocket-advanced-rest-probes.txt`
|
||||
|
||||
## 20. 무엇을 하는 코드인가
|
||||
|
||||
`advanced/presence`(4) · `advanced/codec`(3) + `cbor`(3) + `protobuf`(4) · `advanced/sockjs`(3) · `advanced/graphql`(3) · `advanced/compression`(3) · `advanced/client`(3) · `advanced/release`(1) · `advanced/http2`(1) · `advanced/http3`(1) · `advanced`(1).
|
||||
|
||||
`WebSocketAdvancedCapability`(95)가 능력 카탈로그이고 `propertyName()`이 `"backend.websocket.advanced."` 접두사를 만든다 — **§0에서 확인한 세 네임스페이스 중 아무 `@Configuration`도 읽지 않는 그 접두사다.** 실제 Advanced 게이트는 `app.websocket-platform.advanced.*`를 쓴다.
|
||||
|
||||
바이너리 코덱이 두 벌 있다 — `WebSocketCborCodec`(166) + `CborCodecProfile` + `DuplicateKeyPolicy`, `WebSocketProtobufCodec`(159) + `ProtobufCodecProfile` + `DescriptorCompatibility` + `DescriptorCompatibilityGate`(79). 공통 상위는 `BinaryCodecProfile`(44) + `WebSocketBinaryCodecBackend`(64) + `SchemaParity`(105).
|
||||
|
||||
`advanced/graphql`(3)은 GraphQL-over-WebSocket 브리지(`GraphQlTransportBridge` 163 · `GraphQlTransportBridgePolicy` 71 · `GraphQlCloseCode` 52) — graphql leaf의 `advanced/websocket`(§8, `modelled`)과 짝을 이루는 반대편이다.
|
||||
|
||||
## 21. Negative-space probes
|
||||
|
||||
### 21.1 (8.1) 도달성
|
||||
|
||||
30개 중 `@Configuration`은 `SockJsConfiguration`(103) 하나다. 그것이 `advanced.compression` · `core` · `security`를 import하므로 압축 정책과 core 일부가 그 경로로 도달 가능하다. 나머지 27개는 어떤 설정도 참조하지 않는다.
|
||||
|
||||
### 21.2 (8.2) 조건 형제 비교 — 능력 접두사가 둘이다
|
||||
|
||||
| 출처 | 문자열 | 소비 |
|
||||
|---|---|---|
|
||||
| `advanced/WebSocketAdvancedCapability:76` | `backend.websocket.advanced.<name>` | **없음** |
|
||||
| 네 `@Configuration`의 `@ConditionalOnProperty` | `app.websocket-platform.advanced.<name>` | 있음 |
|
||||
|
||||
graphql §36.2(`VirtualThreadProfile.propertyName()`이 실제 게이트와 다른 이름을 반환)와 같은 형태다. §22.1.
|
||||
|
||||
### 21.3 (8.3) 중복 메커니즘 — 승격 게이트
|
||||
|
||||
`advanced/release/AdvancedPromotionGate`(120)와 `release/WebSocketStableReleaseGate`(106, §11)가 각각 Advanced 승격과 Stable 릴리스를 판정한다. 둘 다 main 참조 0이고 테스트만 있다 — 릴리스 시점 도구이므로 런타임 미배선이 정상이다.
|
||||
|
||||
## 22. Findings
|
||||
|
||||
### 22.1 P3 — 능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다
|
||||
|
||||
§21.2. `WebSocketAdvancedCapability.propertyName()`이 반환하는 `backend.websocket.advanced.*`를 읽는 `@ConditionalOnProperty`가 없다. 운영자가 그 메서드가 알려 주는 키를 설정하면 아무 일도 일어나지 않고, 실제로 능력을 켜는 키는 `app.websocket-platform.advanced.*`다.
|
||||
|
||||
`backend.websocket` 네임스페이스 전체가 소비자를 갖지 않는다는 §4.1의 부분집합이다.
|
||||
|
||||
---
|
||||
|
||||
# Sub-scope 08 — `testkit` + 대체 소스셋 3종 (18 files)
|
||||
|
||||
> 근거 `evidence/raw/226-inbound-websocket-testkit-probes.txt`
|
||||
|
||||
## 23. 무엇을 하는 코드인가
|
||||
|
||||
`testkit`(7)이 `testkit/arch` · `testkit/fault` · `testkit/runtime` 세 갈래로 계약과 하네스를 담는다.
|
||||
|
||||
대체 소스셋 셋이 각각 하나의 IT를 든다:
|
||||
|
||||
```
|
||||
brokerRelayTest/.../advanced/stomp/StompBrokerContractTest.java
|
||||
jettyWebSocketTest/.../runtime/JettyWebSocketRuntimeIT.java
|
||||
nginxWebSocketTest/.../proxy/NginxWebSocketContractIT.java + NginxWebSocketHarness.java
|
||||
nginxWebSocketTest/resources/nginx/nginx.conf + nginx-no-upgrade.conf
|
||||
```
|
||||
|
||||
`build.gradle`이 네 개 레인을 등록한다 — `websocketNginxTest` · `websocketBrokerRelayTest` · `websocketAdvancedTest` · `websocketJettyTest`.
|
||||
|
||||
`nginx-no-upgrade.conf`가 특히 의미 있다 — 프록시가 `Upgrade` 헤더를 전달하지 **않는** 설정을 별도 파일로 두고 그 경우의 계약을 확인한다. WebSocket 배포에서 가장 흔한 운영 실패다.
|
||||
|
||||
## 24. Negative-space probes
|
||||
|
||||
### 24.1 (8.1)·(8.2) 레인이 무엇을 인증하는가
|
||||
|
||||
세 IT가 각각 실제 Jetty · 실제 Nginx 컨테이너 · 실제 브로커를 상대로 돈다. 그런데 그 IT들이 세우는 애플리케이션이 무엇인지가 핵심이다 — `stomp/WebSocketConfig`(배선됨)인가, `PlatformWebSocketHandler`(미배선)인가.
|
||||
|
||||
`JettyWebSocketRuntimeIT`가 `runtime` 패키지에 있고 `testkit/runtime`이 그것을 받친다. inbound-web §48.1에서 확인한 형태("픽스처가 조립하고 레인이 픽스처를 인증한다")가 여기서도 성립하는지는 그 픽스처가 무엇을 등록하는지에 달려 있다.
|
||||
|
||||
### 24.2 (8.4) 레인과 문서
|
||||
|
||||
CLAUDE.md Evidence 절이 인용하는 것은 `WebSocketBoundaryQualificationTest`(랜덤 포트 Tomcat) 하나이고, 네 개 커스텀 레인은 언급되지 않는다.
|
||||
|
||||
## 25. Findings
|
||||
|
||||
### 25.1 P3/기록 — 네 개 커스텀 레인이 CLAUDE.md의 증거 절에 없다
|
||||
|
||||
§24.2. 증거로 인용되는 것은 기본 `test` 레인의 자격 테스트 하나뿐이고, Jetty·Nginx·브로커 릴레이·Advanced 네 레인은 문서에 없다. 그 레인들이 인증하는 것이 면책된 P2 항목(브로커 릴레이)과 미배선 플랫폼(runtime)이므로, 문서가 그것을 증거로 들지 않는 것은 일관되다 — 다만 레인의 존재 자체가 기록되지 않는다.
|
||||
|
||||
---
|
||||
|
||||
# 26. 모듈 종합 — `adapter-inbound-websocket`
|
||||
|
||||
## 26.1 커버리지 원장 정산
|
||||
|
||||
8개 sub-scope, **253 / 253 FULL_READ** · `STRUCTURAL_ONLY 0 · EXCLUDED 0 · UNCLASSIFIED 0`.
|
||||
|
||||
## 26.2 발견 종합 — P2 2건 · P3 5건 *(§4.1은 분석 후 P1 → P2로 하향; §26.6 참조)*
|
||||
|
||||
| 심각도 | § | 발견 |
|
||||
|---|---|---|
|
||||
| **P2** | 4.1 | **`backend.websocket` 플랫폼(약 90개 main 파일)에 조립 지점이 없고 모듈 SSOT 문서에 존재하지 않는다** — 세 안전 장치(`WebSocketPlatformStartupValidator` 125 · `WebSocketStackExclusivity` 78 · `WebSocketPlatformSettings`의 safe-default 규약)가 전부 호출자 0 |
|
||||
| P2 | 13.1 | 연결 티켓·origin 정책·메시지 권한·연결 예산·명령 멱등성이 요청 경로 밖이고, 좁은 STOMP 인터셉터 둘이 대체한다 |
|
||||
| P3 | 22.1 | 능력 프로퍼티 이름 생성기와 실제 게이트가 다른 접두사(`backend.websocket.advanced.*` 대 `app.websocket-platform.advanced.*`) |
|
||||
| P3 | 4.2 | origin 허용목록이 두 네임스페이스에 중복 선언되고 기본값이 정반대(빈 집합 대 `http://localhost:3000`) |
|
||||
| P3 | 7.1 · 10.1 · 25.1 | `ReactiveFrameSink` 참조·테스트 0 · `WebSocketMessageHandler` 참조·테스트 0 · 커스텀 레인 4종이 문서 증거 절에 없음 |
|
||||
|
||||
## 26.3 이 모듈의 성격 — 부분 공시
|
||||
|
||||
169개 main 파일이 세 덩어리로 나뉘고 문서가 그중 하나만 서술한다:
|
||||
|
||||
```
|
||||
8 파일 stomp/ CLAUDE.md 전체가 이것을 서술 · 배선됨 · 랜덤포트 Tomcat 자격 테스트
|
||||
71 파일 advanced/** Evidence 절이 "not claimed"로 일부 면책 · 4개 @Configuration이 부분 조립
|
||||
90 파일 플랫폼 (backend.websocket) 문서 없음 · @Configuration 0 · 안전 장치 셋 전부 미호출
|
||||
```
|
||||
|
||||
**면책은 정확하고 불완전하다.** "Broker relay, multi-node/durable delivery, rollback-safe publication, replay/resume, backpressure, and a versioned domain projection catalog are P2 and are not claimed" — 이 문장이 `advanced/stomp/rabbit`(7) · `advanced/cluster`(9) · `advanced/resume`(8) · `outbound`(8)를 정확히 지목한다. 지목하지 않는 것이 90개 플랫폼 파일이고, 그 안에 "unsafe or incoherent 설정을 거부한다"는 시작 검증기와 "리액티브 엔드포인트가 조용히 응답하지 않는 상태"를 탐지하는 스택 배타성 검사가 있다.
|
||||
|
||||
**세 인바운드 모듈의 공시 스펙트럼:**
|
||||
|
||||
| | inbound-web (14) | inbound-graphql (16) | inbound-websocket (17) |
|
||||
|---|---|---|---|
|
||||
| 미배선 규모 | 다수 | 다수 | 90 / 169 |
|
||||
| 공시 | 없음 (README가 반대 서술) | 등급표 13행 전수 + 전용 테스트 | **부분** — 면책 문장 하나가 24 파일을 덮고 90 파일을 덮지 않음 |
|
||||
| P1 | 6 | 0 | **1** |
|
||||
|
||||
공시의 완성도가 그대로 P1 수에 대응한다.
|
||||
|
||||
**그리고 이 모듈은 inbound-web §40.1을 이름 붙여 탐지하는 코드를 갖고 있다** — `WebSocketStackExclusivity`가 "with both a servlet container and Reactor Netty available it starts the servlet one... serves none of them — no error, no warning"을 서술한다. web에서 29개 파일을 죽인 그 조건을, 이 leaf는 진단 클래스로 만들어 두고 호출하지 않는다.
|
||||
|
||||
## 26.4 완료 게이트
|
||||
|
||||
- [x] denominator 253 / 253 FULL_READ
|
||||
- [x] 8개 sub-scope 각각 §8.1~§8.4 수행 — Spring 애노테이션 전수(169 중 7) · 세 네임스페이스 소비자 추적 · 파일 단위 참조 카운트 · CLAUDE.md 면책 목록 대조
|
||||
- [x] evidence `218`–`226` 생성
|
||||
- [x] 거짓 양성 후보 검증 후 기각: `advanced/**` 미배선(→ Evidence 절이 면책) · `stomp` 어댑터의 좁은 범위(→ CLAUDE.md가 정확히 그 범위를 서술) · `ResumeTokenCodec` 키링 미배선(→ graphql §24.1과 달리 키를 요구하는 검증기가 없어 잘못된 확인 신호가 없음)
|
||||
- [x] 소스 미변경
|
||||
|
||||
## 26.5 실행 검증
|
||||
|
||||
```
|
||||
$ ./gradlew :adapter:inbound:websocket:test
|
||||
BUILD SUCCESSFUL in 17s
|
||||
GRADLE_EXIT=0
|
||||
|
||||
test-results 집계: classes=91 tests=720 failures=0 errors=0 **skipped=0**
|
||||
```
|
||||
|
||||
`build.gradle`이 등록하는 네 개 커스텀 레인(`websocketNginxTest` · `websocketBrokerRelayTest` · `websocketAdvancedTest` · `websocketJettyTest`)은 실행하지 않았다 — Nginx 레인은 Docker 컨테이너를, Jetty 레인은 별도 임베디드 서버를, 브로커 릴레이 레인은 외부 브로커를 요구한다.
|
||||
|
||||
**이 모듈의 P1(§4.1)은 720개 테스트가 전부 통과하는 상태에서 나왔다.** 미배선 플랫폼 90개 파일이 단위 테스트로 덮여 있고, 조립 여부를 묻는 테스트가 없다.
|
||||
|
||||
|
||||
## 26.6 분석 후 판정 변경 — §4.1 P1 → P2
|
||||
|
||||
app-bootstrap(모듈 18) 분석 중 `src/config/architecture/modules.json`의 `runtime_memberships`와 `conditionalTransportTest`의 계약 테스트를 읽고 이 모듈의 등급을 낮췄다.
|
||||
|
||||
```
|
||||
adapter-inbound-websocket runtime_memberships = []
|
||||
adapter-inbound-grpc runtime_memberships = []
|
||||
adapter-inbound-graphql runtime_memberships = ["app-bootstrap"]
|
||||
adapter-inbound-web runtime_memberships = ["app-bootstrap", "sample-portfolio"]
|
||||
```
|
||||
|
||||
```java
|
||||
// ConditionalTransportCompositionContractTest
|
||||
/** Transports that must not reach any runtime: no membership, and no composition-root edge. */
|
||||
BUILD_ONLY_TRANSPORTS = { "adapter-inbound-grpc": GrpcServerConfig,
|
||||
"adapter-inbound-websocket": stomp.WebSocketConfig }
|
||||
```
|
||||
|
||||
> "**gRPC and WebSocket are build-only — no membership, and nothing may put them on a runtime.**"
|
||||
|
||||
**이 leaf는 어떤 출하 런타임에도 올라가지 않는다.** 따라서 §4.1의 실패 시나리오("팀이 `backend.websocket.enabled=true`를 설정하고 엔드포인트가 열리지 않는다")는 현재 출하되는 두 조합(`app-bootstrap` · `sample-portfolio`) 어디에서도 발생할 수 없다. 그것이 P1의 조건("지금 틀린 동작")을 충족하지 않게 만든다.
|
||||
|
||||
**그러나 발견 자체는 남는다.** CLAUDE.md가 "A future composition must deliberately add the registered dependency and set `ca-skeleton.websocket.enabled=true`"로 채택 경로를 명시하므로, 이 leaf는 채택을 전제로 유지된다. 그 채택 시점에 채택자가 마주하는 상태가 §4.1이 서술한 것 — 세 개 네임스페이스, 그중 하나는 소비자 없음, 문서는 8개 파일만 서술 — 이고, 세 안전 장치가 호출되지 않는다는 사실도 그대로다.
|
||||
|
||||
`ConditionalTransportCompositionContractTest`의 다른 문장이 이 상황에 정확히 적용된다 — "**class existence is not composition evidence.**" 이 모듈에는 조립 증거가 없는 클래스가 90개 있고, build-only 등급이 그것을 오늘의 사고에서 면제하되 채택 시점의 부채로 남긴다.
|
||||
|
||||
## Source anchors
|
||||
|
||||
이 문서가 backtick으로 인용한 타입·경로를 저장소 트리에 대고 해석한 결과다. 해석된 것만 싣는다 — 총 **126개** (main 122 · test 1 · 기타 3).
|
||||
|
||||
```
|
||||
src/adapter/inbound/websocket/build.gradle
|
||||
src/config/architecture/modules.json (adapter-inbound-websocket 항목)
|
||||
|
||||
main:
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/WebSocketAdvancedCapability.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/ExternalSessionIndex.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/ExternalSessionSummary.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/FanoutDeduplicator.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/FanoutEnvelope.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/MessagingFanoutAdapter.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/PortBackedExternalSessionIndex.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/ReplayCursor.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/ReplayEventMapper.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/codec/BinaryCodecProfile.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/codec/SchemaParity.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/codec/WebSocketBinaryCodecBackend.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/codec/cbor/CborCodecProfile.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/codec/cbor/DuplicateKeyPolicy.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/codec/cbor/WebSocketCborCodec.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/codec/protobuf/DescriptorCompatibility.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/codec/protobuf/DescriptorCompatibilityGate.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/codec/protobuf/ProtobufCodecProfile.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/codec/protobuf/WebSocketProtobufCodec.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/graphql/GraphQlCloseCode.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/graphql/GraphQlTransportBridge.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/graphql/GraphQlTransportBridgePolicy.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/resume/ResumeCoordinator.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/resume/ResumeDecision.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/resume/ResumeTokenCodec.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/resume/ResumeTokenKeyRing.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/resume/ResumeTokenPayload.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/sockjs/SockJsConfiguration.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/StompAckMode.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/StompAckPolicy.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/StompAuthorizationPolicy.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/StompBrokerExclusivity.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/StompConfiguration.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/StompDestinationCatalog.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/StompEvidence.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/StompProfile.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/StompRefusal.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/StompSecurityInterceptor.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/rabbit/MultiNodeUserDestination.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/rabbit/RabbitBrokerRelayConfiguration.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/rabbit/RabbitBrokerRelayProfile.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/rabbit/UserDestinationPolicy.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/rabbit/UserDestinationRouting.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/stomp/rabbit/UserSessionLocation.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/authz/MessageAuthorizationPolicy.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/budget/WebSocketConnectionBudget.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/codec/StrictWebSocketJsonCodec.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/codec/WebSocketWireTypeManifest.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/config/WebSocketPlatformSettings.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/config/WebSocketPlatformStartupValidator.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/config/WebSocketStackExclusivity.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/core/WebSocketActorReference.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/core/WebSocketConnectionContext.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/core/WebSocketConnectionId.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/core/WebSocketEndpointCatalog.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/core/WebSocketEndpointName.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/core/WebSocketNodeId.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/core/WebSocketSubprotocolName.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/error/WebSocketCloseCode.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/error/WebSocketClosePolicy.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/error/WebSocketErrorMessage.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/error/WebSocketErrorTransport.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/error/WebSocketFailureCategory.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/evidence/WebSocketConnectionEvidence.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/evidence/WebSocketInboundEvidence.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/evidence/WebSocketOutboundEvidence.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/handler/LateResponseTombstone.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/handler/WebSocketCorrelationRegistry.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/handler/WebSocketMessageHandler.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/handshake/HandshakeAdmissionPipeline.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/handshake/HandshakeDecision.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/handshake/HandshakeRequest.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/idempotency/CommandReconciliation.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/idempotency/CommittedResultLedger.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/idempotency/WebSocketCommandKey.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/idempotency/WebSocketCommandOutcome.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/inbound/FragmentAssembler.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/lifecycle/CloseOrchestration.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/lifecycle/HeartbeatPolicy.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/moduleboundary/WebSocketModuleBoundary.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/moduleboundary/WebSocketModulePurity.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/moduleboundary/WebSocketStableModule.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/observability/SafeWebSocketLogFields.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/observability/WebSocketMetricTags.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/ordering/GapDetector.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/ordering/OrderingProfile.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/ordering/StreamSequencer.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/outbound/GlobalBufferBudget.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/outbound/OutboundDelivery.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/outbound/OutboundEnqueueResult.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/outbound/OutboundPriority.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/outbound/OutboundQueue.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/outbound/OutboundQueueSnapshot.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/outbound/SerializedOutboundWriter.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/protocol/WebSocketCatalogFingerprint.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/protocol/WebSocketEnvelope.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/protocol/WebSocketMessageCatalog.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/protocol/WebSocketMessageDescriptor.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/protocol/WebSocketMessageFamily.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/protocol/WebSocketMessageType.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/protocol/WebSocketProtocolProfile.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/protocol/WebSocketSchemaVersion.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/release/WebSocketNginxProxyProfile.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/release/WebSocketRollingRestartScenario.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/release/WebSocketStableReleaseGate.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/security/WebSocketAuthenticationProfile.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/security/WebSocketConnectionTicket.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/security/WebSocketOriginPolicy.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/security/WebSocketTicketStore.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/servlet/PlatformWebSocketHandler.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/servlet/ServletFrameSink.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/session/WebSocketSessionRegistry.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/stomp/AuthenticatedHandshakeInterceptor.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/stomp/LiveEventProjector.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/stomp/LiveEventStompBroadcaster.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/stomp/SafeStompSubProtocolErrorHandler.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/stomp/WebSocketConfig.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/stomp/WebSocketInboundAuthorizationInterceptor.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/stomp/WebSocketProperties.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/webflux/ReactiveFrameSink.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/webflux/WebSocketDataBufferLifecycle.java
|
||||
src/main/java/dev/caskeleton/adapter/inbound/websocket/webflux/WebSocketDataBufferPolicy.java
|
||||
|
||||
test:
|
||||
src/test/java/dev/caskeleton/adapter/inbound/websocket/stomp/WebSocketBoundaryQualificationTest.java
|
||||
|
||||
기타:
|
||||
src/build.gradle
|
||||
src/config/architecture/modules.json
|
||||
src/jettyWebSocketTest/java/dev/caskeleton/adapter/inbound/websocket/runtime/JettyWebSocketRuntimeIT.java
|
||||
|
||||
해석되지 않은 인용 (9종) — 외부 타입·문서상 약칭 등:
|
||||
evidence/raw/218-inbound-websocket-module-inventory.txt
|
||||
evidence/raw/219-inbound-websocket-core-probes.txt
|
||||
evidence/raw/220-inbound-websocket-protocol-probes.txt
|
||||
evidence/raw/221-inbound-websocket-session-probes.txt
|
||||
evidence/raw/222-inbound-websocket-policy-probes.txt
|
||||
evidence/raw/223-inbound-websocket-stomp-probes.txt
|
||||
evidence/raw/224-inbound-websocket-advanced-stomp-probes.txt
|
||||
evidence/raw/225-inbound-websocket-advanced-rest-probes.txt
|
||||
evidence/raw/226-inbound-websocket-testkit-probes.txt
|
||||
|
||||
```
|
||||
Reference in New Issue
Block a user