Files
clean-architecture-backend-…/docs/websocket-superpowers-package/research/source-websocket-deep-research.md
T

62 KiB
Raw Blame History

WebSocket 실시간 양방향 연결 실행 플랫폼 심층 리서치

결론과 기술 기준선

이번 조사에서 가장 중요한 결론은 WebSocket 모듈의 안정성을 “연결이 살아 있고 sendMessage()가 성공했는가”로 정의해서는 안 된다는 것입니다. RFC 6455 WebSocket은 HTTP Upgrade 이후 양방향 메시지를 운반하는 저수준 프로토콜이고, 애플리케이션 메시지의 라우팅·업무 처리 완료·구독·ACK·재전송·Resume 의미를 정의하지 않습니다. Spring Framework 역시 WebSocket 자체는 메시지 내용의 의미를 정의하지 않으므로 STOMP 같은 subprotocol을 협상하거나 애플리케이션 규약을 별도로 만들어야 한다고 설명합니다. citeturn17search0turn7view2

따라서 권장 모델은 제시하신 **접근 C, 공통 Connection Runtime + Protocol Adapter**입니다.

HTTP Handshake / Upgrade
        ↓
WebSocket Connection Runtime
├─ Origin / Authentication / Admission
├─ Connection Context
├─ Local Session Registry
├─ Inbound Assembly / Budget
├─ Outbound Serialized Writer
├─ Bounded Queue / Backpressure
├─ Heartbeat / Idle / Max Age
├─ Security / Observability
├─ Drain / Disconnect
└─ Reconnect Coordination
        ↓
Protocol Adapter
├─ Raw Typed Protocol
├─ STOMP 1.2
├─ GraphQL Subscription Bridge
└─ Provider-specific Protocol
        ↓
Protocol Command / Query / Event
        ↓
Application Use Case
        ↓
JPA / MongoDB / Messaging / Redis / HTTP Client

이 구조의 핵심은 Connection Runtime의 운영 의미와 Protocol Adapter의 메시지 의미를 분리하는 것입니다. STOMP의 ACK, RECEIPT, Destination 의미를 Raw Typed Protocol에 억지로 투영해서도 안 되고, 반대로 자체 Raw Protocol의 messageId, sequence, resumeToken을 STOMP 표준 기능인 것처럼 선언해서도 안 됩니다. STOMP 자체도 Destination 문자열을 opaque한 값으로 취급하며, 실제 전달·신뢰성 의미는 서버와 Destination 구현에 따라 달라진다고 명시합니다. citeturn21search0

현재 기술 기준

2026년 8월 14일 기준 Spring Boot 문서의 Stable은 4.1.0이며, Boot 4.1.0은 Spring Framework 7.0.8+를 요구합니다. Java 최소 요구는 17이고 Java 26까지 호환되므로, Backend Skeleton이 Java 21을 자체 기준선으로 고정하는 것은 충분히 합리적인 플랫폼 정책입니다. Embedded Servlet Container 기준으로 Boot 4.1.0은 Tomcat 11.0.x와 Jetty 12.1.x를 지원합니다. citeturn18search1

Spring Boot 4.1은 embedded Tomcat과 Jetty의 WebSocket 자동 구성을 제공하고 MVC 애플리케이션에서는 spring-boot-starter-websocket을 제공하며, reactive 애플리케이션은 WebSocket API와 spring-boot-starter-webflux 조합을 사용합니다. Spring Boot의 reactive server 지원 범위에는 Reactor Netty, Tomcat, Jetty가 있으며, WebFlux 쪽 기본 운영 후보는 Reactor Netty가 적절합니다. citeturn18search0turn18search3

영역 조사 결론 플랫폼 등급
Java 21 기준선. Spring 최소값보다 플랫폼 기준을 높게 고정 Stable
Spring Boot 4.1 BOM Stable
Spring Framework Boot 관리 7.0.x, 현재 Boot 최소 7.0.8 Stable
Servlet Raw WebSocket Tomcat 기본, Jetty 호환 Lane Stable
Reactive WebSocket WebFlux + Reactor Netty 기본 Stable 선택
Raw Text Protocol UTF-8 JSON Typed Envelope Stable 기본
Raw Binary Protobuf 우선 검토, CBOR 선택 Advanced
STOMP STOMP 1.2 Adapter Advanced Stable
Simple Broker Local/Test·단일 인스턴스 제한 제한 지원
External Broker Relay Broker capability 검증 후 Advanced
SockJS 신규 서비스 기본 제외 Legacy Compatibility
permessage-deflate Endpoint별 명시적 Opt-in Advanced
HTTP/2 WebSocket RFC 8441 경로별 E2E 검증 Compatibility
HTTP/3 WebSocket RFC 9220은 표준이 존재하나 플랫폼 채택은 별도 Experimental
대형 파일 전송 Fileserver/Object Storage 사용 비지원
Durable ACK·DLQ·Replay Messaging 소유 WebSocket 비지원

HTTP/2와 HTTP/3에서 WebSocket을 구성하는 표준 자체는 각각 RFC 8441과 RFC 9220으로 이미 존재합니다. 따라서 “HTTP/3 WebSocket 프로토콜이 실험적”이라고 표현하기보다는, 표준은 존재하지만 Backend Skeleton에서 ClientNginx/IngressRuntime 전체 경로 검증이 끝나지 않았으므로 플랫폼 기능 등급을 Experimental로 둔다고 표현하는 것이 정확합니다. RFC 9220은 HTTP/3의 Extended CONNECT를 WebSocket에 적용합니다. citeturn22search1turn7view3

핵심 질문에 대한 답

한 메시지의 실행 상태는 다음 한 줄로 표현해서는 안 됩니다.

DELIVERED = true / false

대신 적어도 세 증거 축이 필요합니다.

Inbound Evidence
FRAME_RECEIVED
→ MESSAGE_ASSEMBLED
→ MESSAGE_VALIDATED
→ MESSAGE_AUTHORIZED
→ APPLICATION_STARTED
→ APPLICATION_COMMITTED | APPLICATION_FAILED

Outbound Evidence
MESSAGE_CREATED
→ QUEUED
→ WRITE_STARTED
→ WRITTEN_TO_LOCAL_TRANSPORT
→ CLIENT_RECEIVED_ACK?       // 별도 Protocol이 있을 때만
→ CLIENT_APPLIED_ACK?        // 별도 Application ACK가 있을 때만

Connection Evidence
OPEN
→ HEARTBEAT_ALIVE
→ SUSPECTED_HALF_OPEN
→ DRAINING
→ CLOSE_SENT / CLOSE_RECEIVED
→ CLOSED | ABNORMAL

즉,

sendMessage() 성공
≠ Client 수신

Client 수신
≠ Client Application 적용

STOMP RECEIPT
≠ Application Transaction Commit

TCP/WebSocket 연결 유지
≠ Client Application 정상

Application Commit
≠ Response가 Client에게 관측됨

이어야 합니다. STOMP 1.2의 RECEIPT은 해당 Client Frame을 서버가 처리했다는 증거이며 이전 Frame들이 서버에 수신됐다는 누적 증거로 쓸 수 있지만, 규격은 이전 Frame들이 완전히 처리되었다는 뜻은 아니라고 명시합니다. 따라서 RECEIPT을 업무 트랜잭션 커밋 증거로 바꾸어 해석하면 안 됩니다. citeturn11view1turn21search0

이 판단이 전체 플랫폼 설계의 중심축이어야 합니다.

책임 경계와 공개 계층·모듈 구조

인접 플랫폼과의 경계

제시하신 경계는 전반적으로 타당합니다. 특히 Messaging과 WebSocket 사이의 경계가 가장 중요합니다. Spring도 WebSocket을 HTTP와 다른 비동기 메시징 구조라고 설명하지만, 그것이 곧 durable messaging을 의미하지는 않습니다. STOMP 역시 Reliability와 Destination의 실제 의미를 서버별 구현에 맡깁니다. citeturn17search0turn21search0

인접 모듈 WebSocket이 소유 인접 모듈이 소유
web Upgrade 성공 이후 Connection Runtime HTTP Route, Forwarded Header 정규화, HTTP 인증 진입, Upgrade 이전 오류
security 인증 결과를 Connection Context에 유지, 메시지 권한 적용 Token·Session 검증, Actor·Tenant·Permission 원천
messaging 현재 연결 Session으로 Live Push Durable Event, ACK, Retry, Replay, DLQ, Offset
redis Presence·Session Index·Ephemeral fan-out 사용 TTL·원자 연산·Pub/Sub 자체 의미론
graphql WebSocket transport adapter GraphQL operation, subscription, GraphQL error semantics
grpc 브라우저·Client 중심 장기 양방향 연결 내부 서비스 typed RPC와 gRPC streaming
fileserver 파일 상태·reference event 대용량 byte 업·다운로드, Range, 검사
notification “새 알림 있음” live signal Inbox·읽음 상태·채널 전달 상태
jpa·mongodb Application Use Case 호출 Transaction, Query, Repository
httpclient WebSocket 외 일반 outbound HTTP와 분리 HTTP 호출·retry 정책

특히 다음 연결은 금지하는 것이 좋습니다.

WebSocketHandler
  → JpaRepository 직접 호출

@MessageMapping
  → MongoTemplate 직접 상태 전이

WebSocket Session
  → Kafka ACK 의미를 직접 흉내냄

Redis Pub/Sub
  → durable replay라고 선언

WebSocket Binary Frame
  → 대형 파일 업로드

STOMP /queue/**
  → 이름만 보고 durable queue라고 선언

STOMP 규격상 /queue/foo라는 문자열 자체에는 Queue durability 같은 의미가 없습니다. Destination은 서버 구현이 해석하는 opaque string이고, 전달 신뢰성도 Destination과 Broker별 설정에 달려 있습니다. citeturn21search0

공개 기능 계층

권장 공개 계층은 다음과 같습니다.

공개층 공개 대상 포함 기능
WS1 Standard Typed WebSocket 일반 애플리케이션 Endpoint, JSON Typed Message, RequestResponse, Event, Context, Auth, Heartbeat, Bounded Queue
WS2 Advanced Messaging 고급 실시간 서비스 Subscription, Application ACK, Resume, Sequence, Binary Codec, STOMP
WS3 Infrastructure Extension 플랫폼·인프라 Broker Relay, Multi-node Fan-out, Compression, SockJS, H2/H3 Profile
WS4 Admin Plane 운영자 Session Drain, Disconnect, Protocol disable, Connection snapshot, maintenance broadcast

일반 도메인 개발자에게는 WebSocketSession, Reactor Netty native channel, 임의 SimpMessagingTemplate, raw Destination 생성, ConcurrentWebSocketSessionDecorator 구성 등을 직접 노출하기보다 등록된 Endpoint/Profile/Message Catalog를 제공하는 편이 좋습니다. Servlet WebSocket의 underlying standard session은 concurrent send를 직접 안전하게 제공하지 않기 때문에 Spring도 동기화 또는 ConcurrentWebSocketSessionDecorator 사용을 안내합니다. citeturn0search1turn12search0

권장 의존성 구조

제시한 모듈 분리는 그대로 채택할 가치가 높습니다.

modules/websocket/
├── websocket-core-api
├── websocket-protocol
├── websocket-session
├── websocket-security
├── websocket-resilience
├── websocket-observability
│
├── websocket-servlet
├── websocket-webflux
│
├── websocket-stomp
├── websocket-broker-relay
├── websocket-cluster
├── websocket-resume
├── websocket-client
├── websocket-admin
│
├── websocket-spring-boot-starter-mvc
├── websocket-spring-boot-starter-webflux
│
├── websocket-testkit-core
├── websocket-testkit-servlet
├── websocket-testkit-webflux
├── websocket-testkit-browser
└── websocket-testkit-proxy

의존 방향은 다음처럼 단방향으로 고정하는 것이 좋습니다.

core-api
   ↑
protocol / session / security / resilience / observability
   ↑
servlet              webflux
   ↑                    ↑
starter-mvc        starter-webflux

stomp
  └─ broker-relay

resume
  └─ messaging bridge abstraction

cluster
  └─ redis / messaging capability adapter

admin
  └─ session abstraction

core-api에는 Jakarta WebSocket, Servlet, Reactor, Netty, STOMP 타입을 넣지 않는 편이 좋습니다. 같은 논리로 MVC와 WebFlux Starter도 상호 배타적으로 두는 것이 안전합니다. Spring Boot는 servlet과 reactive 스택을 별도로 구성하며, spring-boot-starter-webspring-boot-starter-webflux가 함께 있으면 기본적으로 MVC를 선택하므로 우연한 Stack 선택을 피하는 정책이 필요합니다. citeturn18search3

기본 Starter에서는 다음을 제외하는 것이 적절합니다.

websocket-stomp
websocket-broker-relay
websocket-cluster
websocket-resume
websocket-client
SockJS
permessage-deflate
HTTP/2 WebSocket Profile
HTTP/3 WebSocket Profile

이는 “지원하지 않는다”가 아니라 WS1의 기본 런타임을 가볍고 예측 가능하게 유지하면서 WS2·WS3 기능은 명시적으로 선택하게 한다는 의미입니다.

Handshake·인증·보안·Typed Protocol 계약

Handshake는 HTTP와 WebSocket의 경계선이다

Classic WebSocket은 HTTP request로 시작하여 성공 시 101 Switching Protocols로 전환됩니다. Spring Framework도 Upgrade 전과 후가 전혀 다른 프로그래밍 모델임을 강조하며, Upgrade 이후에는 한 연결을 통해 애플리케이션 메시지가 계속 흐릅니다. citeturn17search0turn7view2

권장 파이프라인은 다음과 같습니다.

HTTP Request
→ Trusted Proxy / Forwarded Header 정규화
→ WebSocket Endpoint Profile 선택
→ Host / Origin 검증
→ HTTP Authentication 또는 Connection Ticket 검증
→ Actor / Tenant 후보 Context 생성
→ Subprotocol 협상
→ Extension 협상
→ Connection / Tenant / IP Admission
→ 101 Switching Protocols
→ Protocol-level Authentication 완료
→ Session OPEN

Upgrade 이전에는 기존 web 플랫폼의 HTTP 오류 계약을 그대로 사용할 수 있습니다.

Handshake 상황 권장 HTTP 결과
Handshake 구조 오류 400
필수 HTTP 인증 실패 401
Origin·Endpoint 권한 거부 403
존재 은닉이 필요한 Endpoint 404
중복 Connection 정책 충돌 409
연결 Rate Limit 429
Drain·과부하 Admission 거부 503

반대로 101 이후에는 HTTP ProblemDetail을 보낼 수 없으므로 Typed ERROR message 또는 WebSocket Close code로 전환해야 합니다. RFC 6455에서도 101 이외의 Handshake response는 HTTP semantics를 유지하지만 성공적으로 protocol switch가 완료된 뒤에는 WebSocket framing으로 통신합니다. citeturn8view2turn17search0

Origin 검증은 필수 보안 경계

Spring Security 공식 문서는 브라우저의 WebSocket 연결에는 일반적인 Same Origin Policy가 자동 적용되지 않으므로 서버가 이를 명시적으로 보호해야 한다고 강조합니다. Cookie 인증 상태에서 Origin을 무제한으로 허용하면 다른 사이트가 사용자의 인증 상태를 이용하는 Cross-Site WebSocket Hijacking 문제가 생길 수 있습니다. Spring Security는 STOMP 구성에서 CONNECT에 CSRF token을 요구하는 방식을 제공합니다. citeturn17search1

Stable 기본 정책은 다음이 적절합니다.

Origin
→ Exact Allowlist

Wildcard Subdomain
→ 기본 금지, 등록 Profile만 허용

null Origin
→ 기본 거부

Cookie Authentication
→ Origin 검증 필수

STOMP + Cookie Session
→ CONNECT CSRF 사용

Cross-origin Token Profile
→ Endpoint별 명시 Opt-in

다음은 기본 금지로 두는 것이 좋습니다.

allowedOrigins = *
Cookie Authentication + Origin 미검증
요청 Origin을 그대로 Allow
장기 Access Token을 query parameter에 사용
모든 STOMP MESSAGE / SUBSCRIBE permitAll
Client가 보낸 actorId / tenantId 신뢰

Spring Security는 STOMP에서 inbound MESSAGESUBSCRIBE를 Destination별로 통제할 수 있으며, 특히 broker prefix로 직접 MESSAGE를 보내 시스템 발신자를 가장하거나 다른 사용자용 Destination을 SUBSCRIBE하는 형태를 막아야 한다고 설명합니다. citeturn17search1

브라우저 인증 Profile

브라우저 표준 WebSocket 생성 인터페이스는 URL과 subprotocol을 중심으로 제공되며 애플리케이션이 일반 HTTP client처럼 임의의 Authorization 헤더를 자유롭게 추가하는 인터페이스는 제공하지 않습니다. 반면 Handshake는 브라우저 credential 정책에 따라 Cookie 등의 인증정보를 사용할 수 있습니다. citeturn7view0turn5search1

따라서 다음과 같이 분리하는 것이 좋습니다.

인증 Profile 지원 등급 권장 의미
Cookie / HTTP Session Stable HTTP 인증 Principal 승계 + Exact Origin
One-time Connection Ticket Stable 권장 Bearer를 URL에 장기 노출하지 않는 브라우저 연결
STOMP CONNECT Bearer Advanced ChannelInterceptor에서 인증
Query Long-lived Access Token 비지원 Proxy·Access Log·History 노출 위험
Protocol 중간 Re-auth Experimental 복잡성이 크므로 초기 Stable 제외

Spring Security는 HTTP Handshake에서 인증된 Principal을 WebSocket으로 넘겨주는 모델을 지원합니다. STOMP에서 별도 token 인증을 원할 경우 CONNECT frame의 header를 ChannelInterceptor에서 처리할 수 있습니다. citeturn17search1turn5search1

One-time Ticket은 표준 기능이 아니라 플랫폼 자체 Profile로 설계해야 합니다.

POST /websocket-tickets
→ ticket 발급

Ticket:
- random high-entropy identifier
- 매우 짧은 TTL
- one-time atomic consumption
- actor binding
- tenant binding
- endpoint binding
- origin binding
- clientInstanceId 선택 binding

그리고 Access Log에는 Ticket 전체 값을 남기지 않는 것이 적절합니다.

장기 Connection의 Credential 만료 정책은 Stable에서 **“만료·권한회수 시 현재 Connection 종료 → 새 Credential로 재연결”**을 기본으로 잡는 편이 낫습니다. Connection 내부 reauthentication은 state machine·race condition·권한 회수 처리 비용이 커지므로 Advanced/Experimental로 남기는 편이 안전합니다.

Subprotocol은 Production Endpoint에서 명시적으로 협상

WebSocket 표준은 Sec-WebSocket-Protocol로 상위 protocol을 협상할 수 있습니다. Spring도 STOMP 등의 고수준 protocol을 이 header를 통해 선택하는 것을 지원합니다. citeturn8view2turn17search0

권장 Raw protocol 이름은 다음처럼 Major version + codec을 포함하는 형태입니다.

hyeonworks.realtime.v1.json
hyeonworks.realtime.v1.protobuf
v12.stomp
graphql-transport-ws

Production Typed Endpoint는:

지원되는 Subprotocol 하나 선택
→ 성공

지원되는 공통 Protocol 없음
→ Handshake 거부

로 처리하고, subprotocol 없이 “아무 JSON이나 받아들이는” 모드는 Local 또는 Compatibility profile로 제한하는 것을 권장합니다.

Typed Message Envelope

Stable Raw JSON protocol에는 무조건 모든 필드를 넣는 것이 아니라 공통 식별 필드 + 메시지 종류별 선택 필드를 두는 편이 좋습니다.

{
  "type": "document.updated",
  "version": 1,
  "messageId": "01K...",
  "correlationId": "01K...",
  "streamId": "document:abc",
  "sequence": 42,
  "occurredAt": "2026-08-14T06:00:00Z",
  "expiresAt": "2026-08-14T06:01:00Z",
  "payload": {}
}

권장 의미는 다음과 같습니다.

필드 계약
type 등록된 stable wire name
version 해당 message schema major/version
messageId 메시지 인스턴스 식별
correlationId RequestResponse 연결
causationId 필요 시 원인 message
streamId Ordering·Resume 대상 logical stream에서만
sequence 해당 stream 내부 monotonically increasing sequence
occurredAt 서버 기준 이벤트 시각
expiresAt 오래된 Command 재실행 방지용 선택 필드
payload Message type별 DTO

모든 메시지에 sequence, idempotencyKey, subscriptionId를 강제하기보다 메시지 family별 schema를 만드는 것이 좋습니다.

RequestMessage<T>
ResponseMessage<T>
CommandMessage<T>
EventMessage<T>
SubscribeMessage<T>
AckMessage
ErrorMessage
ResumeMessage
SnapshotMessage<T>

다음은 금지하는 것이 적절합니다.

messageType = Java FQCN
payload = Map<String, Object>
Entity 자체 직렬화
Java Serialization
무제한 polymorphic deserialization
한 Envelope 안에 모든 command payload union 수작업

Codec 정책

Codec 등급 정책
UTF-8 JSON Stable 기본 Browser 친화적, Contract Test 필수
Protobuf Binary Advanced 권장 강한 schema가 필요한 고성능 client
CBOR Advanced 선택 실제 요구·SDK 지원이 있을 때
Raw Binary 제한 사전 등록된 Message Profile만
Java Serialization 비지원 Wire contract로 사용하지 않음

대용량 byte는 Typed Event에서 object/file reference를 전달하고 fileserver/object-storage가 byte transport를 담당하도록 유지하는 것이 좋습니다.

실행 증거·Idempotency·Ordering·ACK·Resume 계약

증거 모델은 플랫폼의 핵심 API여야 한다

질문의 핵심인 “어디까지 갔는가”를 정확히 답하려면 다음 단계가 필요합니다.

단계 서버가 증명할 수 있는 것 재호출 판단
FRAME_RECEIVED WebSocket frame이 runtime에 도착 업무 실행 여부는 모름
MESSAGE_ASSEMBLED Fragment 조립 완료 아직 재실행 안전
MESSAGE_VALIDATED Protocol/schema 검증 완료 아직 업무 미실행
MESSAGE_AUTHORIZED Transport/message 권한 통과 아직 업무 미실행
APPLICATION_STARTED Use Case 진입 Commit 여부 불명확 가능
APPLICATION_COMMITTED 업무 결과의 durable evidence 존재 다시 실행하지 않고 결과 Replay
APPLICATION_FAILED 정의된 실패로 종료 실패 유형에 따라 재시도
RESPONSE_QUEUED Outbound queue에 등록 Client 수신 증거 아님
WRITE_STARTED local transport 쓰기 시작 Client 수신 증거 아님
WRITTEN_LOCALLY framework/local transport 단계 완료 Client 적용 증거 아님
CLIENT_ACKED protocol ACK를 Client가 보냄 ACK 정의 범위만 증명
CLIENT_APPLIED app-level 적용 ACK가 존재 Client application 반영 증거

여기서 APPLICATION_COMMITTED는 WebSocket runtime 자체가 추측하면 안 됩니다. Application transaction과 Idempotency/Result ledger가 durable evidence를 제공해야 합니다.

예:

Command(commandId, idempotencyKey)
        ↓
Application Use Case
        ↓
DB Transaction
├─ Domain State 변경
└─ Command Result / Idempotency Record 저장
        ↓ COMMIT
APPLICATION_COMMITTED

그 뒤 소켓이 끊겨도 결과는 다음 연결에서 재조회할 수 있어야 합니다.

Commit 후 Response 유실

가장 위험한 케이스는 다음입니다.

Client
  → COMMAND C42

Server
  → APPLICATION_STARTED
  → DB COMMIT
  → Response 생성
  → Socket write 시작

Network
  → 연결 단절

Client
  → Response 미관측

이때 Client의 올바른 상태는 FAILED가 아니라:

COMPLETION_UNKNOWN_TO_CLIENT

입니다.

그리고 재연결 후:

COMMAND C42 재전송
     ↓
Idempotency Ledger 조회
     ├─ COMPLETED
     │    → 저장된 Result 반환
     │
     ├─ PROCESSING
     │    → 진행 상태 반환
     │
     ├─ ABSENT
     │    → 새 실행
     │
     └─ 동일 key + 다른 fingerprint
          → conflict

으로 처리해야 합니다.

따라서 상태 변경용 WebSocket Command에는 다음 중 하나가 필수입니다.

idempotencyKey
client-generated commandId
client-generated resourceId
reconciliation query

Connection sequence만으로 mutation idempotency를 보장하면 안 됩니다. Connection은 끊기고 다시 만들어질 수 있고, Resume window 역시 업무 idempotency TTL과 동일한 개념이 아니기 때문입니다.

중요한 mutation이 이미 HTTP나 gRPC에서 명확하게 표현되고 있다면 다음 구조도 우선 검토할 가치가 있습니다.

HTTP/gRPC
→ Command 수행 + Idempotency

WebSocket
→ 결과·상태 변경 Live Event 전달

이렇게 하면 WebSocket이 mutation transport와 durable command ledger까지 모두 소유하는 복잡성을 크게 줄일 수 있습니다.

STOMP RECEIPT과 ACK의 정확한 의미

STOMP 1.2에서:

  • RECEIPT은 요청한 Frame에 대해 서버가 처리했다는 protocol-level 응답입니다.
  • 이전 Frame들이 서버에 수신됐다는 누적 증거로는 사용할 수 있지만, 이전 모든 Frame의 최종 업무 처리를 보장하지 않습니다.
  • ack:auto는 별도 Client ACK가 필요 없습니다.
  • ack:client는 cumulative acknowledgment입니다.
  • ack:client-individual은 개별 메시지 acknowledgment입니다.
  • Connection이 ACK 전에 실패하면 서버가 메시지를 재전달할 수 있으나 구체적인 redelivery semantics는 서버 구현에 의존합니다. citeturn11view1turn11view2turn21search0

따라서 플랫폼 용어를 다음과 같이 분리해야 합니다.

TRANSPORT_WRITE
PROTOCOL_RECEIPT
BROKER_DELIVERY
BROKER_ACK
APPLICATION_COMMIT
CLIENT_RECEIVED
CLIENT_APPLIED

이 단어들을 서로 alias하지 않는 것이 중요합니다.

Ordering은 WebSocket Frame 순서와 Application 순서가 다르다

하나의 연결에서 네트워크 바이트의 순서가 유지되더라도, Spring STOMP의 clientInboundChannelclientOutboundChannel은 thread pool 기반으로 처리되므로 application handling과 publish 결과가 서로 다른 thread에서 수행되어 원래 순서와 달라질 수 있습니다. Spring은 setPreserveReceiveOrder(true)setPreservePublishOrder(true)를 제공하지만, 순서 보장에는 성능 비용이 있다고 명시합니다. citeturn20search1

따라서 다음 profile이 적절합니다.

Ordering Profile 사용 예 계약
UNORDERED_LOW_LATENCY presence, typing 순서 의존 금지
SESSION_ORDERED 한 connection command chain session별 serialize
SUBSCRIPTION_ORDERED subscription update subscription별 sequence
STREAM_KEY_ORDERED document/room/entity stream logical stream별 sequence

특히 Multi-node와 Resume를 고려한다면 sessionSequence보다:

streamId + streamSequence

가 더 중요한 복구 기준입니다.

예:

document:123
  seq=100
  seq=101
  seq=102

Client는:

lastReceivedSequence
lastAppliedSequence

를 구분해야 합니다.

lastReceivedSequence=102지만 UI/State Store에는 101까지만 반영하다 브라우저가 죽을 수도 있기 때문입니다. Resume 기준은 일반적으로 **lastAppliedSequence**가 더 안전합니다.

Subscription 계약

권장 Subscription request는 다음 정도면 충분합니다.

{
  "type": "subscribe",
  "version": 1,
  "messageId": "...",
  "payload": {
    "subscriptionId": "sub-01",
    "topic": "document.changes",
    "resourceId": "doc-123",
    "resumeFrom": 101
  }
}

다음은 서버 등록 Catalog에 의해 통제합니다.

Topic Allowlist
Filter Allowlist
Projection Profile
Maximum subscriptions / connection
Maximum subscriptions / actor
Event rate
Outbound queue budget
Authorization profile
Resume capability

권한은 “Connection할 때 로그인했는가”와 “이 Event를 지금 받을 권리가 있는가”를 구분해야 합니다. Spring Security 역시 MESSAGE와 SUBSCRIBE의 Destination 권한을 구별하며, outbound 자체를 모두 검사하는 대신 subscription을 엄격히 보호하는 방식을 설명합니다. citeturn17search1

민감한 스트림에서 권한 회수가 즉시 반영되어야 한다면 다음 중 하나가 필요합니다.

Event delivery 시 Authorization 재검증
또는
Permission Revocation Event → subscription revoke / session close
또는
짧은 Connection Max Age

모든 Event마다 DB Authorization Query를 수행하는 것은 비용이 크므로 Endpoint별 정책으로 두는 것이 좋습니다.

Reconnect와 Resume

RFC 6455에는 끊어진 Application Stream의 replay cursor나 resume semantics가 정의돼 있지 않습니다. 끊어진 뒤에는 새 WebSocket Connection을 만들고 애플리케이션 protocol이 복구를 정의해야 합니다. citeturn7view2turn17search0

Stable Resume protocol은 다음 형태가 적절합니다.

Client disconnect
        ↓
Exponential Backoff + Jitter
        ↓
새 Handshake + 새 Authentication
        ↓
RESUME
{
  streamId,
  resumeToken,
  lastAppliedSequence,
  snapshotVersion
}
        ↓
Server
├─ history available
│    → replay sequence+1 ...
│
├─ history compacted / gap too old
│    → SNAPSHOT_REQUIRED
│
├─ permission changed
│    → RESUME_DENIED
│
└─ token expired
     → RESUME_EXPIRED

Resume 성공 뒤에도 Client는 duplicate detection을 수행해야 합니다.

seq <= lastAppliedSequence
→ duplicate, ignore

seq == lastAppliedSequence + 1
→ apply

seq > lastAppliedSequence + 1
→ GAP, stop incremental apply
→ resume/snapshot request

Replay source는 WebSocket Node의 메모리가 아니라 Messaging/Event Log 등 durable capability여야 합니다. WebSocket Resume module은 cursor와 snapshot orchestration만 담당하는 것이 모듈 경계에 맞습니다.

Presence는 사실이 아니라 관측 결과다

WebSocket OPEN만 보고:

user.online = true

라고 업무 사실을 선언하면 안 됩니다. 네트워크 partition, background browser, delayed heartbeat, proxy timeout 때문에 실제 사용 상태와 Socket 관측 상태가 일치하지 않을 수 있기 때문입니다.

권장 모델은:

lastObservedAt
activeConnectionCount
lastHeartbeatAt
presenceState = ONLINE | IDLE | STALE | OFFLINE

처럼 관측 시각이 포함된 상태입니다.

Redis에는 Session 객체 자체가 아니라:

actor fingerprint
connection count
node id
lastObservedAt
TTL

정도의 summary만 저장하는 것이 적절합니다.

런타임·Backpressure·Heartbeat·멀티인스턴스·STOMP 운영

Servlet과 WebFlux 실행 모델

Servlet WebSocket에서는 동일 Session에 여러 thread가 동시에 write하는 구조를 피해야 합니다. Spring의 ConcurrentWebSocketSessionDecorator는 하나의 thread가 실제 send를 수행하도록 하고 send-time limit 및 buffer-size limit을 적용할 수 있습니다. Buffer overflow 처리 전략도 제공됩니다. citeturn12search0turn0search1

따라서 Servlet runtime은 다음 구조로 고정하는 것을 권장합니다.

Application Event
    ↓
SessionOutboundQueue
    ↓
Serialized Writer
    ↓
ConcurrentWebSocketSessionDecorator
    ↓
Container Session
Application thread
→ WebSocketSession.sendMessage 직접 호출

을 일반 API로 노출하지 않는 것이 좋습니다.

WebFlux에서는 WebSocketSession.receive()가 inbound Flux<WebSocketMessage>를, send(Publisher<WebSocketMessage>)가 outbound 완료를 나타내는 reactive API를 제공합니다. Reactor Netty 등의 pooled buffer를 async boundary 뒤까지 보관할 경우 DataBuffer retain/release 수명도 고려해야 합니다. citeturn15view0turn4view3turn4view4

다만 Reactive Streams를 쓴다는 이유만으로 브라우저까지 end-to-end backpressure가 자동 제공된다고 선언하면 안 됩니다. 브라우저 WebSocket API에는 reactive demand protocol이 없으므로 서버의 bounded buffering과 message-level 정책은 여전히 필요합니다. 브라우저가 송신할 때는 bufferedAmount로 아직 network에 전달되지 않은 application data byte 수를 관찰할 수 있습니다. citeturn7view0

Slow Consumer 정책

모든 message type에 같은 overflow policy를 적용하면 안 됩니다.

Message 성격 Queue 초과 시
업무 Command 결과 Drop 금지, disconnect + reconciliation/resume
업무 상태 전이 Event Drop 금지, disconnect + durable resume
Presence / Typing DROP 또는 COALESCE 허용
최신 가격·상태 Snapshot COALESCE_BY_KEY 가능
Durable Event local buffer 무한 확대 금지, connection close 후 cursor replay
Admin/security notice 우선순위 Queue 또는 즉시 close

권장 overflow policy catalog:

DISCONNECT
DROP_LATEST
DROP_OLDEST
COALESCE_BY_KEY
SNAPSHOT_REQUIRED

DROP_*는 메시지 schema가 explicitly lossy라고 선언한 경우에만 허용해야 합니다.

Spring STOMP도 client outbound가 느릴 때 한 thread가 실제 send를 하고 추가 메시지가 buffer에 쌓이는 구조이며 sendTimeLimitsendBufferSizeLimit을 제공하므로, 무제한 buffering을 피해야 합니다. Spring 문서는 clientInboundChannelclientOutboundChannel의 thread pool 및 queue 설정 또한 성능에 직접 영향을 준다고 설명합니다. citeturn20search0

초기 Resource Budget

아래 값은 프로토콜 표준값이 아니라 성능 시험을 시작하기 위한 Backend Skeleton 초기 profile 권고값입니다. 서비스별 부하 시험 후 올리는 방식이 안전합니다.

항목 Stable 시작값 후보 비고
Raw JSON assembled message 64 KiB 더 큰 payload는 별도 Profile
Binary message 256 KiB Advanced
STOMP inbound message 64 KiB Spring STOMP client 기본 inbound limit도 64 KiB citeturn19search0
JSON nesting depth 32 Codec guard
Array elements 1,000 Message schema가 더 낮게 설정 가능
String bytes 32 KiB field별 더 낮은 제한 권장
Subscriptions / connection 32 Profile별 조정
In-flight requests 32 무제한 correlation 금지
Outbound queue 512 KiB + message count limit byte와 count 모두 제한
Send stall limit 10 s 시작값 실제 proxy/network 시험 필요
Global buffered bytes 반드시 상한 Heap 보호
Connection / actor endpoint별 명시 browser multi-tab 고려
Reconnect rate actor·IP·tenant별 제한 reconnect storm 방지

여기서 Frame size, assembled WebSocket Message size, STOMP Message size, decoded JSON size, outbound queue size는 각각 별도 제한이어야 합니다. Spring STOMP 자체도 WebSocket message를 조립해 더 큰 STOMP message를 구성할 수 있으며 이를 위해 별도의 message size limit을 제공합니다. citeturn20search0turn19search0

Heartbeat 계층을 분리해야 한다

다음은 서로 같은 기능이 아닙니다.

TCP Keepalive
WebSocket Ping/Pong
STOMP Heartbeat
Application Heartbeat
Nginx proxy_read_timeout
Presence TTL

RFC 6455에서 Ping을 받은 endpoint는 closing 상태가 아니라면 Pong으로 응답해야 하며 Ping/Pong은 keepalive와 peer responsiveness 확인에 사용할 수 있습니다. citeturn8view3

STOMP heartbeat는 CONNECTCONNECTEDheart-beat 값으로 양측 송신 능력과 수신 희망 간격을 교환하고 각 방향의 실제 최소 간격을 계산합니다. citeturn21search0

Nginx 공식 WebSocket proxy 문서는 upstream server가 아무 데이터도 보내지 않으면 기본적으로 60초 후 연결을 종료한다고 설명하며, proxy_read_timeout을 늘리거나 WebSocket Ping을 주기적으로 보내 연결 활동과 생존 확인을 수행할 수 있다고 명시합니다. citeturn17search2turn16view1

따라서 설정 관계를 다음처럼 계약화해야 합니다.

heartbeatInterval
    < heartbeatTimeout
    < proxyReadTimeout

예를 들어 실제 Profile을 25s heartbeat / 55s failure / 75s proxy read처럼 잡을 수 있지만, 구체 숫자는 Nginx·Ingress·LB·모바일 네트워크 시험 결과로 결정하는 편이 좋습니다.

Application heartbeat는 transport heartbeat와 별도로 정말 필요한 경우만 사용합니다. 예를 들어 “Client app event loop가 정상적으로 state를 적용하고 있는지”가 중요하면 app-level PING/PONG 또는 state watermark를 별도 정의할 수 있습니다.

Browser outbound budget

브라우저의 WebSocket.bufferedAmountsend()한 데이터 중 아직 network로 전달되지 않은 byte 수를 표시하므로 Client SDK의 로컬 backpressure 신호로 유용합니다. 다만 이것은 상대 서버가 메시지를 받았다는 ACK가 아닙니다. citeturn7view0

Client SDK는 다음을 가져야 합니다.

maxBufferedAmount
bounded command queue
message priority
expiresAt
retryability
idempotency requirement
offline queue policy

특히 offline 상태에서 mutation을 무제한 저장한 뒤 재접속 시 전부 보내면 오래된 Command가 뒤늦게 실행될 수 있으므로:

expiresAt
+
idempotencyKey
+
explicit offline-capable flag

가 필요합니다.

Compression

RFC 7692 permessage-deflate는 Opening Handshake에서 협상하는 per-message compression extension입니다. server_no_context_takeover, client_no_context_takeover, server_max_window_bits, client_max_window_bits 등의 파라미터로 양 방향 압축 context와 memory footprint를 제어할 수 있습니다. citeturn22search0

따라서 Stable 기본은:

permessage-deflate = OFF

가 적절하고 다음을 측정한 Endpoint만 Opt-in하는 것이 좋습니다.

bandwidth 절감률
CPU / connection
memory / connection
p95/p99 send latency
decompressed size
compression context memory
sensitive data + attacker-controlled input 위험

Binary Protobuf처럼 이미 compact한 payload는 compression 효율이 낮을 수 있으므로 Codec별 benchmark가 필요합니다.

Multi-instance Session 구조

실제 socket은 연결을 받은 application instance가 소유하므로 구조를 다음처럼 나누는 것이 좋습니다.

Node A
├─ actual WebSocketSession
├─ local subscription handlers
├─ local outbound queue
└─ local writer

External Registry
├─ connectionId → nodeId
├─ actor → active node summaries
├─ subscription summary
└─ TTL / lastObservedAt

Fan-out Capability
├─ ephemeral: Redis capability
└─ durable: Messaging capability

Native Session 객체를 Redis에 직렬화하여 다른 Node로 이동시키는 구조는 금지해야 합니다.

Sticky session도 다음 문제를 해결하지 못합니다.

Pod restart
Node crash
Deployment
Reconnect
Resume history
Lost event

따라서 sticky routing은 최적화일 수 있어도 recovery contract가 되어서는 안 됩니다.

STOMP 지원 범위

Spring의 Simple Broker는 시작하기 쉬우나 STOMP 명령의 subset만 지원하고 ACK·RECEIPT 등을 지원하지 않으며 clustering에 적합하지 않습니다. Spring은 production-scale broadcast를 위해 external broker relay를 별도 옵션으로 제공합니다. citeturn19search1turn20search0

기능 Simple Broker Broker Relay
Local Pub/Sub 지원 지원
SEND·SUBSCRIBE 기본 지원 Broker 지원 범위
ACK 제한/비지원 Broker capability
RECEIPT 제한/비지원 Broker capability
Cluster 부적합 가능
Durable queue 보장하지 않음 Broker·Destination 설정에 따라
Redelivery 보장하지 않음 Broker 설정에 따라
DLQ 없음 Broker capability
Transaction 제한 Broker capability
User Destination Spring 변환 가능 Broker와 결합 검증
운영 권장 Local/Test·단일 node Advanced production

Spring Broker Relay는 애플리케이션과 외부 Broker 사이에서 TCP 연결을 사용해 메시지를 양 방향 relay합니다. 따라서 Broker Relay를 채택할 때는 WebSocket socket 수뿐 아니라 Broker connection footprint, broker failover, heartbeat, reconnection, broker-side destination lifecycle을 별도로 부하 시험해야 합니다. citeturn19search1

Multi-server User Destination은 Spring의 user-destination/registry broadcast 기능을 이용해 다른 application server에 연결된 사용자를 찾는 구성이 가능하지만, 이 역시 Broker의 temporary queue 정리 및 destination semantics와 함께 검증해야 합니다. citeturn3search7turn3search3

오류·Close·Proxy·Shutdown·관측성 계약

Error Message와 Close는 분리

Message 단위 오류가 발생했다고 항상 Connection을 끊는 것은 좋지 않습니다.

Recoverable message error
→ ERROR message
→ Connection 유지

Connection-scoped fatal error
→ Close

가 기본 원칙이어야 합니다.

RFC/IANA WebSocket Close code 기준에서 주요 코드는 다음과 같습니다. citeturn9view2turn9view3turn8view0turn9view4turn21search1

Close Code 의미 플랫폼 사용
1000 Normal Closure 정상 종료
1002 Protocol Error frame/subprotocol 위반
1003 Unsupported Data 지원하지 않는 data type
1007 Invalid Payload Data invalid UTF-8 등
1008 Policy Violation 일반 protocol/security policy
1009 Message Too Big size limit
1011 Internal Error 예기치 못한 server failure
1012 Service Restart rolling restart/drain
1013 Try Again Later 일시 과부하
40004999 Private Use application close catalog

IANA registry는 1012를 Service Restart, 1013을 Try Again Later로 등록하고 40004999를 Private Use 범위로 둡니다. citeturn21search1turn21search3

권장 private catalog는 다음과 같습니다.

4400 INVALID_MESSAGE
4401 AUTHENTICATION_REQUIRED
4403 ACCESS_DENIED
4408 HEARTBEAT_TIMEOUT
4409 DUPLICATE_CONNECTION
4422 VALIDATION_FAILED
4429 RATE_LIMITED
4503 OVERLOADED

다만 VALIDATION_FAILED 같은 Message 단위 오류는 일반적으로 Close보다 Typed ERROR가 우선입니다. Private Close는 “이 Connection을 더 이상 유지할 수 없는 이유”에 사용해야 합니다.

Browser WebSocket API에서 script가 직접 close()에 지정할 수 있는 code는 1000 또는 30004999 범위이며, reason은 UTF-8 기준 123 bytes 이하여야 합니다. 따라서 Client-visible close reason에는 stack trace·SQL·token·PII를 넣으면 안 됩니다. citeturn7view0

Nginx와 TLS

Nginx reverse proxy에서 UpgradeConnection은 hop-by-hop header이므로 upstream으로 자동 전달되지 않으며 WebSocket proxying을 위해 명시적으로 처리해야 합니다. Nginx 공식 구성도 UpgradeConnection을 별도로 설정합니다. citeturn17search2

기본 계약은 다음처럼 두는 것이 좋습니다.

location /ws/ {
    proxy_pass http://backend;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # deployment profile에 맞춰 명시
    proxy_read_timeout ...;
    proxy_send_timeout ...;
}

Nginx 최신 문서 기준으로 proxy_read_timeout 기본값은 60초이며 “전체 응답 시간”이 아니라 두 successive read 사이 timeout입니다. WebSocket처럼 idle할 수 있는 장기 연결에서는 반드시 heartbeat profile과 정렬해야 합니다. citeturn16view0turn16view1

Forwarded header와 외부 URL 신뢰 모델은 web 모듈에서 이미 정한 정책을 재사용해야 합니다.

Client
→ Host Nginx
    → untrusted Forwarded 제거
    → trusted X-Forwarded-* 재작성
→ Backend
→ web platform normalization
→ WebSocket handshake context

WebSocket이 독자적인 X-Forwarded-For parsing 로직을 만들면 web 플랫폼과 client IP·scheme·host 결과가 달라질 수 있습니다.

TLS 정책은 다음이 적절합니다.

Local
→ ws 허용

Dev / Staging / Prod
→ wss 필수

TLS termination
→ Nginx 또는 trusted ingress

Backend plaintext
→ trusted internal network profile에서만

Origin / External Host
→ normalized web context 사용

Graceful Shutdown

Spring Boot는 servlet/reactive server의 graceful shutdown을 제공하지만, “Client가 어떤 sequence부터 다시 받아야 하는지”나 “어떤 WebSocket Command를 이제 받지 말아야 하는지”는 application protocol의 책임입니다. 따라서 WebSocket 플랫폼은 별도의 drain state machine을 가져야 합니다. citeturn0search7

권장 흐름:

Readiness OFF
→ 신규 Handshake Admission 차단
→ 기존 Connection 상태 DRAINING
→ SERVER_DRAINING Control Message
   {
     reconnectAfter,
     resumeSupported,
     deadline
   }
→ 신규 Subscription 거부
→ 신규 mutation Command 거부
→ 이미 commit 중인 Command 제한 시간 처리
→ Outbound Queue 제한 시간 drain
→ Close 1012 Service Restart
→ shutdown deadline 초과 시 강제 close

Connection이 무기한 지속되는 것을 허용하기보다:

maxConnectionAge
credentialExpiresAt
serverDrainDeadline
resumeWindow

를 두는 편이 Rolling Update 운영에 유리합니다.

Outbound WebSocket Client

Backend가 외부 WebSocket provider와 연결하는 기능은 일반 httpclient retry profile을 그대로 사용하지 않는 것이 맞습니다. WebSocket client는 “RPC 재시도”가 아니라 Connection 재수립 + Protocol 재협상 + Subscription 재등록 + Resume 문제이기 때문입니다.

Spring WebFlux WebSocket client는 Reactor Netty, Tomcat, Jetty, 표준 Java WebSocket client 구현을 지원합니다. citeturn15view0

권장 Named profile:

websocket:
  clients:
    market-feed:
      uri: wss://provider.example/stream
      protocol: provider.feed.v2
      tls: provider-ca
      connect-timeout: 5s
      heartbeat: 20s
      idle-timeout: 60s
      max-message-size: 64KiB
      max-buffered-bytes: 512KiB
      reconnect:
        min-backoff: 500ms
        max-backoff: 30s
        jitter: true
      resume:
        supported: true

Profile에는 최소:

URI
TLS
Proxy
Subprotocol
Authentication
Handshake timeout
Heartbeat
Idle timeout
Max age
Message limits
Reconnect strategy
Resume strategy
Observability

를 포함해야 합니다.

관측성 모델

장기 연결 하나에 거대한 tracing span 하나를 유지하기보다 Handshake span + message operation span + connection metric 구조가 운영상 더 적합합니다.

권장 Metric:

영역 Metric
Connection active, opened, rejected, closed, duration, abnormal close
Heartbeat ping, pong, timeout, suspected half-open
Reconnect attempt, success, resume success/failure
Message inbound/outbound count·bytes
Validation schema failure, unknown type/version
Security authentication/authorization reject
Application started, committed, failed
Ordering duplicate, sequence gap
Backpressure queue bytes/messages, slow consumer, drop, coalesce
Subscription active, rejected, event lag
STOMP broker availability, receipt timeout, relay disconnect

허용 tag:

endpointProfile
protocol
protocolVersion
messageTypeCatalog
operationCatalog
closeCode
outcome
node
resumeOutcome

금지 tag:

sessionId
connectionId
userId
tenantId raw
resourceId
messageId
subscriptionId
destination의 동적 부분
token
payload

Connection ID 같은 값은 로그 필드나 trace correlation에 제한적으로 사용할 수 있어도 Metric tag로 사용하면 cardinality가 폭증하므로 금지하는 것이 좋습니다.

Access log/event log 예:

timestamp
connectionFingerprint
actorFingerprint
endpointProfile
protocol
node
event = OPEN | CLOSE | RESUME | DRAIN
closeCode
duration
bytesIn
bytesOut

Admin plane은:

Connection Summary
Protocol Version Usage
Node별 active count
Slow Consumer count
Close Code distribution
Resume failure
Broker status

를 제공하되 payload/token/raw filters는 노출하지 않는 것이 좋습니다.

다음 관리 작업은 모두 Audit 대상입니다.

Session Disconnect
Actor Session Disconnect
Tenant Drain
Endpoint Drain
Protocol Version Disable
Maintenance Broadcast
Resume State 수동 무효화

지원 등급·테스트 전략·구현 로드맵

최종 기능 지원 매트릭스

Capability 최종 권고 등급 완료 조건
Raw JSON Typed WebSocket Stable Servlet·WebFlux contract + browser/proxy test
RequestResponse Stable correlation, timeout, cancellation, late response 정의
Mutation Command Stable 조건부 Idempotency ledger 필수
Typed Event Stable bounded queue + schema catalog
Server heartbeat Stable proxy timeout E2E 검증
Exact Origin policy Stable 필수 browser CSWSH test
Cookie/session auth Stable Origin + security integration
One-time ticket auth Stable 권장 atomic single-use + TTL
Connection max age Stable reconnect/drain test
Session serialized writer Stable 필수 concurrency stress test
Bounded outbound queue Stable 필수 slow-consumer test
Sequence / Gap detection Stable for ordered streams duplicate/gap contract
Resume Advanced durable replay source 필요
Snapshot fallback Advanced history-lost test
Subscription Advanced auth·limits·ordering 정의
Application ACK Advanced 의미 명시 + ledger 필요 여부 결정
STOMP 1.2 Advanced Stable protocol matrix
Simple Broker Local/Test cluster 사용 금지
Broker Relay Advanced real broker fault test
Multi-node fan-out Advanced cross-node integration test
Presence Advanced TTL·stale semantics
Protobuf Binary Advanced generated client compatibility
CBOR Advanced 실제 client 수요가 있을 때
permessage-deflate Advanced Opt-in CPU/memory/security benchmark
Outbound WS client Advanced reconnect/resume profile
SockJS Legacy 명시적 legacy requirement
HTTP/2 WS Compatibility end-to-end matrix
HTTP/3 WS Experimental end-to-end support 검증
GraphQL WS semantics WebSocket에서 비소유 GraphQL adapter만
Durable Replay/DLQ 비지원 Messaging 사용
Large file 비지원 Fileserver 사용
Java serialization 비지원
WebSocket exactly-once 비지원 선언 idempotent business operation으로 대체

핵심 Contract Test

Mock WebSocketSession만으로 Stable을 선언해서는 안 됩니다. Spring의 실제 Servlet/WebFlux runtime, Nginx, TLS, browser를 모두 거쳐야 low-level connection semantics를 검증할 수 있습니다. Spring 자체도 Servlet WebSocket과 Reactive WebSocket의 실행 API가 다르고, STOMP에서는 별도 thread pools·buffers·broker relay가 개입합니다. citeturn17search0turn20search0turn15view0

Handshake·Security

101 정상 연결
malformed handshake
unsupported subprotocol
missing subprotocol
Origin allowed / rejected
null Origin
Cookie session
expired session
one-time ticket success
ticket replay
ticket expiration
STOMP CONNECT token
CSRF CONNECT
Forwarded header spoof
Host spoof
connection rate limit
draining endpoint

Spring Security가 WebSocket에서 Same-Origin 방어와 STOMP CONNECT CSRF를 별도로 강조하므로 이 테스트는 Release Gate에 포함해야 합니다. citeturn17search1

Message·Schema

normal JSON
binary
fragmentation
invalid UTF-8
malformed JSON
unknown message type
unknown schema version
unknown enum
oversized string
oversized array
deep JSON
assembled size overflow
compression
decompressed oversize

Execution Evidence·Idempotency

FRAME_RECEIVED 전 disconnect
MESSAGE_VALIDATED 후 reject
APPLICATION_STARTED 후 failure
APPLICATION_COMMITTED 직후 socket reset
response queue 전 disconnect
write 시작 후 disconnect
동일 commandId 재전송
동일 idempotencyKey + 동일 fingerprint
동일 idempotencyKey + 다른 fingerprint
ledger PROCESSING 상태 crash
commit 후 reconnect + reconciliation

이 테스트가 플랫폼의 핵심 질문에 가장 직접적으로 답합니다.

Ordering

동일 Session 병렬 inbound
동일 Subscription 병렬 event
preserveReceiveOrder off/on
preservePublishOrder off/on
cross-node event
duplicate sequence
missing sequence
out-of-order sequence
reconnect 경계 sequence

Spring STOMP의 ordering option이 기본 thread-pool reorder를 보완하는 기능이므로 해당 설정의 비용과 효과를 실제 throughput test에서 비교해야 합니다. citeturn20search1

Backpressure

slow browser
blocked network
queue bytes cap
queue message cap
send time cap
DROP_LATEST
DROP_OLDEST
COALESCE_BY_KEY
critical message overflow
global buffer exhaustion
browser bufferedAmount 증가
WebFlux slow subscriber

Spring STOMP의 outbound send도 slow client에서 buffer가 증가할 수 있어 send-time과 buffer-size limit이 별도로 제공됩니다. citeturn20search0

Heartbeat·Network

Ping/Pong 정상
Pong 손실
Server Ping 정지
half-open
Nginx proxy_read_timeout
TCP reset
mobile network switch
browser sleep
background tab
temporary packet loss
TLS termination restart

Nginx 기본 60초 idle timeout과 WebSocket Ping 사용 가능성을 실제 배포 설정에 맞춰 검증해야 합니다. citeturn17search2

Resume

normal resume
new node resume
lastAppliedSequence 정상
duplicate event
sequence gap
history compacted
snapshot fallback
resume token expired
resume token replay
permission revoked
schema version changed
stream deleted

Multi-instance

Node A client connection
Node B business event
A로 cross-node fan-out
Node A kill -9
Node C reconnect
registry TTL cleanup
stale registry entry
duplicate session registration
network partition between app and fan-out

STOMP

CONNECT / CONNECTED
heartbeat negotiation
SEND
SUBSCRIBE / UNSUBSCRIBE
ACK auto
ACK client
ACK client-individual
NACK
RECEIPT
ERROR
broker disconnect
broker reconnect
simple broker limitation
external relay
user destination multi-node
ordered publication

STOMP ACK mode와 redelivery 의미는 규격 및 Broker별 capability를 함께 검증해야 합니다. citeturn21search0turn19search1

장애·보안·성능 Gate

성능 시험에서는 단순 messages/sec 하나만 보지 말고 다음을 함께 측정해야 합니다.

Concurrent connections / node
Handshake RPS
Reconnect RPS
Idle connection heap
Idle connection direct memory
Thread count
Event-loop utilization
Inbound messages/sec
Outbound messages/sec
p50 / p95 / p99 message latency
queue bytes / connection
global buffered bytes
slow consumer ratio별 처리량
serialization CPU
compression CPU
broker relay latency
resume replay throughput
snapshot latency
GC pause
connection drain duration

특히 다음 부하 시나리오가 중요합니다.

정상 Client 100%
slow Client 1%
slow Client 10%
slow Client 50%

동시에:
Node restart
Broker latency
Redis latency
Reconnect storm

Slow client 몇 개 때문에 전체 outbound thread pool이나 direct memory가 고갈되지 않는지 검증해야 합니다. Spring도 outbound 성능이 client network speed에 크게 영향을 받고 별도의 send/buffer limit이 필요하다고 설명합니다. citeturn20search0

Browser matrix는 최소:

Chromium
Firefox
WebKit

Foreground
Background tab
Sleep / wake
Offline / online
Wi-Fi ↔ Mobile network
Browser close
Page navigation

까지 포함하는 것이 좋습니다.

Runtime matrix:

Tomcat + Nginx + TLS
Jetty + Nginx + TLS
Reactor Netty + Nginx + TLS

를 Stable gate로 잡고, HTTP/2·HTTP/3는 별도 compatibility lane에서 검증합니다. Boot 4.1의 공식 servlet container 기준은 Tomcat 11.0.x와 Jetty 12.1.x이며 reactive server는 Reactor Netty·Tomcat·Jetty를 지원합니다. citeturn18search1turn18search3

단계별 구현 순서와 완료 조건

기준선·경계 확정

먼저 websocket-core-api, MVC/WebFlux starter의 상호 배타성, web·security·messaging·redis와의 의존 방향을 확정합니다.

완료 조건:

Boot 4.1 BOM
Java 21
Tomcat / Reactor Netty 기본 profile
인접 모듈 dependency rule
금지 API architecture test

Raw Typed Stable Runtime

다음으로 STOMP 없이 Raw JSON부터 완성하는 것이 좋습니다.

Handshake
Origin
Subprotocol
Connection Context
JSON Envelope
Message Catalog
RequestResponse
Error
Serialized Writer
Size Limit
Heartbeat
Observability

완료 조건은 실제 Chromium + Nginx + Tomcat/Reactor Netty에서 기본 시나리오가 통과하는 것입니다.

실행 증거와 상태 변경 안전성

플랫폼의 가장 중요한 단계입니다.

Application Started evidence
Commit evidence abstraction
Idempotency capability bridge
Result ledger
Completion Unknown
Reconciliation

완료 조건:

DB Commit 직후 Network reset
→ Client retry
→ business mutation은 1회
→ 이전 Result 회수 가능

이 시나리오가 자동화 테스트로 증명되어야 합니다.

Backpressure·Ordering·Resource Budget

Bounded queue
Serialized writer
Slow consumer classification
Sequence
Gap detection
Ordering profile
Global buffer admission
Browser bufferedAmount policy

완료 조건은 slow-client stress 중에도 fast-client p99와 server memory가 설정된 범위에서 유지되고, critical event drop이 발생하지 않는 것입니다.

Reconnect·Resume

resumeToken
lastAppliedSequence
durable event cursor
deduplication
gap detection
snapshot fallback

완료 조건은 Node A 강제 종료 후 Node B/C에 reconnect해도 중복 없이 최신 state로 수렴하는 것입니다.

Multi-node·STOMP

Raw protocol의 cluster path를 먼저 검증한 뒤 STOMP adapter를 붙이는 것이 좋습니다.

External Session Index
Fan-out adapter
STOMP 1.2
Simple Broker local profile
External Broker Relay
User Destination
Broker outage

Spring Simple Broker는 clustering에 적합하지 않으므로 multi-node Stable 여부는 external fan-out 또는 Broker Relay 시험으로 판단해야 합니다. citeturn19search1turn20search0

고급·호환 기능

마지막에 다음을 추가합니다.

Protobuf
CBOR
permessage-deflate
Outbound WebSocket Client
SockJS
HTTP/2 WebSocket
HTTP/3 WebSocket
Admin Plane

각 기능은 기본 Starter를 비대하게 만들지 않고 별도 module/profile로 승격합니다. Compression은 RFC 7692 협상·memory control과 실제 CPU/heap benchmark가 완료된 Endpoint만 활성화해야 합니다. citeturn22search0

최종 플랫폼 계약

이번 조사 결과를 가장 압축해서 표현하면 다음과 같습니다.

WebSocket Runtime이 보장하는 것
=
연결 수명
+ 인증된 Connection Context
+ Typed Protocol 진입
+ bounded resource usage
+ serialized outbound write
+ heartbeat / disconnect
+ execution evidence 관측
+ reconnect / resume orchestration

그러나 다음은 보장하지 않습니다.

WebSocket Frame 전송
≠ Business Commit

Business Commit
≠ Response Delivery

Response Delivery
≠ Client Applied

Connection Sequence
≠ Business Idempotency

STOMP RECEIPT
≠ Transaction Commit

STOMP ACK
≠ 보편적인 Durable Exactly-once

WebSocket Reconnect
≠ Stream Resume

Simple Broker
≠ Clustered Durable Broker

Presence OPEN
≠ 사용자가 실제 Online이라는 절대 사실

WebSocket 자체와 Spring의 low-level API가 제공하지 않는 이 의미들을 플랫폼이 명시적으로 분리해야 합니다. WebSocket은 content semantics를 규정하지 않는 transport이고, STOMP 역시 Destination과 reliability의 실제 의미를 server implementation에 맡기며, Spring Simple Broker 또한 ACK·Receipt와 clustering에 한계가 있습니다. citeturn17search0turn21search0turn19search1

따라서 최종 권고 구조는 다음입니다.

                    ┌───────────────────────┐
                    │       HTTP / Web       │
                    │ Handshake, Proxy, Auth │
                    └───────────┬───────────┘
                                │ 101
                                ▼
┌─────────────────────────────────────────────────────┐
│                WebSocket Platform                   │
│                                                     │
│ Connection Context │ Session │ Budget │ Heartbeat  │
│ Security │ Ordering │ Backpressure │ Evidence      │
│ Observability │ Drain │ Reconnect Coordination     │
└───────────────┬───────────────────┬─────────────────┘
                │                   │
       ┌────────▼────────┐   ┌──────▼─────────┐
       │ Raw Typed JSON │   │  STOMP Adapter │
       │ / Protobuf     │   │  Broker Relay  │
       └────────┬────────┘   └──────┬─────────┘
                │                   │
                └────────┬──────────┘
                         ▼
               Application Use Case
                         │
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
        JPA/Mongo     Messaging       Redis
        Commit        Replay/DLQ   Registry/TTL
          │              │
          └───────┬──────┘
                  ▼
       Durable Execution Evidence
                  │
                  ▼
          WebSocket Live Delivery

이 모델에서 WebSocket은 “실시간 전달”을 소유하고, Application은 “상태 변경의 진실”을 소유하며, Messaging은 “내구성 있는 이벤트 이력”을 소유합니다. 그 경계가 지켜져야 APPLICATION_COMMITTED, RESPONSE_NOT_OBSERVED, CLIENT_APPLIED, RESUME_FROM_SEQUENCE를 서로 혼동하지 않고 질문하신 핵심 문제—“서버에 도착했는가, 커밋됐는가, 프레임이 나갔는가, 클라이언트가 적용했는가, 어디부터 재개할 수 있는가”—에 각각 독립적인 증거로 답할 수 있습니다.