Skip to content

Latest commit

 

History

History
188 lines (145 loc) · 15.1 KB

File metadata and controls

188 lines (145 loc) · 15.1 KB

04. STOMP over WebSocket

무엇을 / 왜

지금까지(01~03장)는 "프레임을 주고받는" 저수준 채널을 다뤘다. 하지만 실시간 애플리케이션은 보통 더 높은 수준의 의미를 원한다 — "이 메시지를 /topic/prices 구독자 전원에게 보내라", "이 사용자에게만 보내라". 이런 목적지 기반 메시징을 WebSocket 위에서 표준적으로 표현하는 것이 STOMP(Simple Text Oriented Messaging Protocol) 서브프로토콜이다.

spring-websocketmessaging 패키지는 WebSocket 프레임을 STOMP로 해석하고, 그것을 spring-messaging의 메시지 채널/브로커 세계로 연결하는 다리다. 핵심 통찰: 이 패키지는 STOMP 라우팅을 스스로 하지 않는다. WebSocket 프레임 ↔ spring-messagingMessage를 번역하는 경계만 담당하고, 실제 라우팅(목적지 매칭, 구독 관리, 브로커 릴레이)은 spring-messaging에 위임한다.

핵심 타입

   WebSocket 세계                          spring-messaging 세계
 ┌──────────────────┐                    ┌──────────────────────────┐
 │ WebSocketSession │                    │  clientInboundChannel    │
 │  (프레임)         │                    │  clientOutboundChannel   │
 └────────┬─────────┘                    │  (Message<byte[]>)       │
          │                              └──────────────────────────┘
          ▼                                          ▲
 ┌─────────────────────────────────────────────┐    │
 │       SubProtocolWebSocketHandler            │────┘  양방향 브리지
 │  - WebSocketHandler  (프레임 ← 클라이언트)    │
 │  - MessageHandler    (메시지 → 클라이언트)    │
 │  - Map<protocol, SubProtocolHandler>         │
 └────────────────────┬────────────────────────┘
                      │ 프로토콜별 위임
                      ▼
 ┌─────────────────────────────────────────────┐
 │              SubProtocolHandler              │
 │   handleMessageFromClient(...) → outChannel  │
 │   handleMessageToClient(session, msg)        │
 │       └ StompSubProtocolHandler              │  STOMP 1.0/1.1/1.2
 └─────────────────────────────────────────────┘
          │ 인코딩/디코딩            │ 이벤트 발행
          ▼                         ▼
 StompEncoder / StompDecoder   Session{Connect,Connected,
 (spring-messaging)            Subscribe,Unsubscribe,Disconnect}Event

SubProtocolHandler — 한 서브프로토콜의 번역기

SubProtocolHandler는 WebSocket 메시지와 spring-messagingMessage를 양방향으로 번역하는 계약이다.

List<String> getSupportedProtocols();
void handleMessageFromClient(WebSocketSession session, WebSocketMessage<?> message, MessageChannel outputChannel);
void handleMessageToClient(WebSocketSession session, Message<?> message);
String resolveSessionId(Message<?> message);
void afterSessionStarted(WebSocketSession session, MessageChannel outputChannel);
void afterSessionEnded(WebSocketSession session, CloseStatus closeStatus, MessageChannel outputChannel);

handleMessageFromClient는 클라이언트가 보낸 WebSocket 프레임을 해석해 Message로 만들어 출력 채널(clientInboundChannel)로 보내고, handleMessageToClient는 반대로 애플리케이션이 보낸 Message를 WebSocket 프레임으로 인코딩해 세션에 쓴다. resolveSessionId는 메시지에서 대상 세션 ID를 뽑아낸다 — 아웃바운드 라우팅의 열쇠다.

SubProtocolWebSocketHandler — 양방향 브리지

SubProtocolWebSocketHandler는 이 모듈에서 가장 중요한 클래스 중 하나다. 두 인터페이스를 동시에 구현한다.

  • WebSocketHandler — 클라이언트→애플리케이션 방향. WebSocket 콜백을 받아 적절한 SubProtocolHandler로 위임.
  • MessageHandler — 애플리케이션→클라이언트 방향. clientOutboundChannel을 구독해 메시지를 받아 해당 세션으로 전달.

생성자는 인바운드/아웃바운드 두 채널을 받는다.

public SubProtocolWebSocketHandler(MessageChannel clientInboundChannel,
        SubscribableChannel clientOutboundChannel) { ... }

SmartLifecycle.start()에서 아웃바운드 채널을 구독(clientOutboundChannel.subscribe(this))하고, stop()에서 구독 해제 후 모든 활성 세션을 GOING_AWAY로 닫는다.

인바운드(클라이언트 → 애플리케이션):

public void afterConnectionEstablished(WebSocketSession session) {
    checkSessions();                              // 유휴 세션 정리(아래)
    session = decorateSession(session);           // ConcurrentWebSocketSessionDecorator로 감쌈
    this.sessions.put(session.getId(), new WebSocketSessionHolder(session));
    findProtocolHandler(session).afterSessionStarted(session, this.clientInboundChannel);
}

public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
    ...
    SubProtocolHandler protocolHandler = findProtocolHandler(session);
    protocolHandler.handleMessageFromClient(session, message, this.clientInboundChannel);
    holder.setHasHandledMessages();
}

세션이 열리면 ConcurrentWebSocketSessionDecorator로 감싼다(01장). STOMP 메시지는 여러 스레드에서 동시에 한 세션으로 푸시될 수 있으므로 전송 직렬화가 필수다. 그 다음 findProtocolHandler로 협상된 서브프로토콜에 맞는 핸들러를 골라 위임한다 — 협상된 프로토콜이 없고 핸들러가 하나뿐이거나 기본 핸들러가 설정돼 있으면 그것을 쓰고, 여러 개인데 협상이 안 됐으면 예외.

아웃바운드(애플리케이션 → 클라이언트):

public void handleMessage(Message<?> message) {   // MessageHandler
    String sessionId = resolveSessionId(message);  // 어느 세션으로?
    WebSocketSessionHolder holder = this.sessions.get(sessionId);
    if (holder == null) return;                     // 세션 이미 사라짐
    findProtocolHandler(session).handleMessageToClient(session, message);
    // SessionLimitExceededException → 통계 기록 후 세션 강제 종료
}

아웃바운드 채널에서 받은 메시지의 세션 ID를 풀어 해당 세션을 찾고, 프로토콜 핸들러로 클라이언트에 보낸다. 전송 한도 초과(SessionLimitExceededException, 01장의 동시성 데코레이터가 던짐)면 통계를 올리고 세션을 정리·종료한다.

유휴 세션 보호(checkSessions / timeToFirstMessage): WebSocket 연결은 맺어졌지만(프록시 문제 등으로) STOMP 프레임이 한 번도 흐르지 않으면, 상위 프로토콜이 협상되지 않은 채 좀비 세션이 매달린다. SubProtocolWebSocketHandler는 연결 후 timeToFirstMessage(기본 60초) 동안 메시지를 한 번도 못 받은 세션을 SESSION_NOT_RELIABLE로 닫는다. 그래서 핸들러가 WebSocketSessionHolderhasHandledMessages 플래그를 둔다.

통계(Stats): 내부 DefaultStats는 세션을 WebSocket / HTTP-streaming / HTTP-polling으로 분류해(03장의 StreamingSockJsSession/PollingSockJsSessioninstanceof로 판별) 카운트하고, 비정상 종료(전송 한도 초과, 무메시지 종료, 전송 오류)도 집계한다. 운영 모니터링의 기반이다.

StompSubProtocolHandler — STOMP 구현

StompSubProtocolHandlerSubProtocolHandler의 STOMP 구현으로 STOMP 1.0/1.1/1.2를 지원한다. 인바운드에서는 BufferingStompDecoder로 WebSocket 프레임을 STOMP 프레임으로 디코딩하고, 아웃바운드에서는 StompEncoder(또는 큰 프레임 분할용 SplittingStompEncoder)로 인코딩한다.

중요한 세부:

  • 메시지 분할 처리 — STOMP 클라이언트(stomp.js 등)는 큰 STOMP 메시지를 16K 경계에서 여러 WebSocket 프레임으로 쪼갠다. 그래서 서버 입력 버퍼는 최소 MINIMUM_WEBSOCKET_MESSAGE_SIZE(16K + 256)를 허용해야 하며, 핸들러는 BufferingStompDecoder를 세션별(Map<String, BufferingStompDecoder>)로 두어 조각을 재조립한다.
  • CONNECT/CONNECTED — 클라이언트의 STOMP CONNECT(또는 STOMP) 프레임을 받으면 인증된 사용자 이름을 CONNECTED 프레임의 user-name 헤더(CONNECTED_USER_HEADER)에 실어 응답하도록 처리한다.
  • 이벤트 발행ApplicationEventPublisherAware로 이벤트 퍼블리셔를 주입받아, STOMP 생명주기마다 애플리케이션 이벤트를 쏜다(아래).
  • 오류 처리StompSubProtocolErrorHandler(SubProtocolErrorHandler 구현)로 클라이언트에 보내는 ERROR 프레임을 커스터마이징하거나 억제할 수 있다.

서브프로토콜 이벤트 — 메시징 생명주기 관찰

messaging 패키지는 STOMP 세션의 사건을 Spring 애플리케이션 이벤트로 노출한다. 모두 AbstractSubProtocolEvent(원본 Message<byte[]>Principal 보유)를 상속한다.

AbstractSubProtocolEvent
   ├ SessionConnectEvent       클라이언트 CONNECT 수신
   ├ SessionConnectedEvent     서버가 CONNECTED로 응답(연결 확립)
   ├ SessionSubscribeEvent     SUBSCRIBE 수신
   ├ SessionUnsubscribeEvent   UNSUBSCRIBE 수신
   └ SessionDisconnectEvent    세션 종료

@EventListener로 이들을 구독하면 접속 사용자 추적, 감사 로깅, 입장/퇴장 알림 같은 기능을 비침투적으로 붙일 수 있다.

DefaultSimpUserRegistry — 누가 접속해 있나

DefaultSimpUserRegistry(spring-messagingSimpUserRegistry 구현)는 위 이벤트들을 수신해 현재 접속 사용자와 그들의 세션·구독을 추적한다. "특정 사용자에게 보내기"(user destination)나 접속자 목록 조회의 백본이다.

동작 흐름 — STOMP 메시지의 왕복

[클라이언트] SEND /app/greet ──WebSocket frame──▶ SubProtocolWebSocketHandler.handleMessage
                                                        │ findProtocolHandler → STOMP
                                                        ▼
                              StompSubProtocolHandler.handleMessageFromClient
                                                        │ 디코딩 → Message<byte[]>
                                                        ▼
                                            clientInboundChannel.send(message)
                                                        │
                ┌───────────────────────────────────────┴──────────────────────┐
                ▼ (spring-messaging 영역)                                          
   SimpAnnotationMethodMessageHandler        →  @MessageMapping 메서드 실행          
   (WebSocketAnnotationMethodMessageHandler)     반환값을 brokerChannel로 발행        
                │                                                                  
                ▼                                                                  
   SimpleBroker / StompBrokerRelay  → 구독자(/topic/...) 매칭                       
                │                                                                  
                ▼                                                                  
        clientOutboundChannel.send(message)                                        
                │                                                                  
                ▼                                                                  
   SubProtocolWebSocketHandler.handleMessage(Message)  ← MessageHandler로 구독 중   
                │ resolveSessionId → 대상 세션                                       
                ▼                                                                  
   StompSubProtocolHandler.handleMessageToClient → 인코딩 → session.sendMessage     
                │                                                                  
                ▼                                                                  
[클라이언트] MESSAGE 프레임 수신 ◀──WebSocket frame──                                

SubProtocolWebSocketHandlerStompSubProtocolHandler가 다루는 것은 이 그림의 양 끝(WebSocket ↔ 채널)뿐이다. 가운데의 @MessageMapping 디스패치, 브로커, 구독 매칭은 전부 spring-messaging이 맡는다. 다만 이 모듈은 STOMP 환경에 특화된 WebSocketAnnotationMethodMessageHandler를 제공해, @MessageMapping 핸들러가 WebSocket 세션 속성·헤더에 접근할 수 있게 보강한다.

핵심 메서드 정리

  • SubProtocolWebSocketHandler.afterConnectionEstablished — 세션을 동시성 데코레이터로 감싸 등록하고 프로토콜 핸들러에 시작을 알림 + 유휴 세션 청소.
  • SubProtocolWebSocketHandler.handleMessage(WebSocketMessage) / handleMessage(Message) — 인바운드/아웃바운드 두 방향의 위임 진입점.
  • findProtocolHandler — 협상된 서브프로토콜로 핸들러 선택, 데코레이터를 벗겨 getAcceptedProtocol 조회.
  • StompSubProtocolHandler.handleMessageFromClient / handleMessageToClient — STOMP 프레임 ↔ Message 인코딩/디코딩과 이벤트 발행.

설계 포인트 / 확장점

  • 경계만 담당, 라우팅은 위임: 이 패키지는 WebSocket ↔ spring-messaging 번역에 집중하고, 목적지 라우팅·브로커는 spring-messaging에 맡겨 관심사를 깔끔히 분리한다.
  • 서브프로토콜 다형성: SubProtocolWebSocketHandler는 프로토콜별 SubProtocolHandler 맵을 들고 있어 STOMP 외 다른 서브프로토콜도 같은 골격에 끼울 수 있다.
  • 운영 안전장치 내장: 동시 전송 직렬화, 전송 한도 초과 시 종료, 무메시지 좀비 세션 정리, 세분화된 통계가 기본 탑재.
  • 이벤트 기반 관찰: 접속/구독/종료를 애플리케이션 이벤트로 노출해 사용자 추적·감사를 비침투적으로 구현.

정리

STOMP over WebSocket은 두 인터페이스를 겸하는 SubProtocolWebSocketHandler가 WebSocket 세계와 spring-messaging 채널 세계를 양방향으로 잇는 다리다. 인바운드 프레임은 StompSubProtocolHandler가 디코딩해 clientInboundChannel로 보내고, 아웃바운드 메시지는 clientOutboundChannel을 구독해 세션 ID로 대상을 찾아 인코딩해 내보낸다. 실제 목적지 라우팅은 spring-messaging이 처리하며, 이 모듈은 번역·세션 관리·운영 안전장치·생명주기 이벤트를 책임진다.