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>
53 KiB
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_READ253 /STRUCTURAL_ONLY0 /EXCLUDED0 /UNCLASSIFIED0 - 최초 분석 revision
a24ece9c→ 재검증 revision21234e38· 이 리프의 변경 파일 0 - 재검증 증거:
EVD-333(소스 드리프트 0),EVD-334(lane 재실행)
재검증이 확인한 것은 대상이 움직이지 않았다는 사실이지, 아래 서술이 옳다는 보증이 아니다. 이번 사이클에서 코드에 대고 다시 확인한 항목은 이 문서의 검증 절과 위 증거가 가리키는 범위다.
분석 대상
src/adapter/inbound/websocket· revisiona24ece9cf797f7ea647e33bf846b115208ed1ba5분모 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.
allowedOriginsis empty andenabledis 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).
가장 무거운 결과는 세 안전 장치가 실행되지 않는다는 것이다:
WebSocketPlatformStartupValidator(125) — "Refuses to start a deployment whose WebSocket configuration is unsafe or incoherent." 호출자 0.WebSocketStackExclusivity(78) — 서블릿/리액티브 이중 스택에서 "reactive endpoints ... simply never answer"를 탐지. 호출자 0.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가 서술하는 모듈 전체다.
// 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 완료 게이트
- denominator 253 / 253 FULL_READ
- 8개 sub-scope 각각 §8.1~§8.4 수행 — Spring 애노테이션 전수(169 중 7) · 세 네임스페이스 소비자 추적 · 파일 단위 참조 카운트 · CLAUDE.md 면책 목록 대조
- evidence
218–226생성 - 거짓 양성 후보 검증 후 기각:
advanced/**미배선(→ Evidence 절이 면책) ·stomp어댑터의 좁은 범위(→ CLAUDE.md가 정확히 그 범위를 서술) ·ResumeTokenCodec키링 미배선(→ graphql §24.1과 달리 키를 요구하는 검증기가 없어 잘못된 확인 신호가 없음) - 소스 미변경
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"]
// 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