지금까지는 서버가 연결을 "받는" 쪽이었다. 하지만 Spring 애플리케이션이 WebSocket 클라이언트가 되어야 할 때도 많다 — 다른 WebSocket 서버에 붙어 데이터를 받거나, 서버 간 STOMP 메시징을 하거나, 통합 테스트에서 자기 서버에 접속할 때다. client, sockjs.client, messaging 패키지의 클라이언트 측 타입들이 이를 담당한다.
설계가 우아한 지점은 클라이언트가 서버와 같은 WebSocketHandler 추상을 재사용한다는 것이다. 연결 방향만 반대일 뿐, 메시지를 받는 콜백 모델은 동일하다. 이 챕터는 저수준 WebSocketClient, 연결을 생명주기로 자동화하는 WebSocketConnectionManager, 폴백까지 갖춘 SockJsClient, 그리고 STOMP를 얹는 WebSocketStompClient를 본다.
┌─────────────────────────────────────────────────────────┐
│ WebSocketClient │
│ execute(handler, uriTemplate, vars) → CF<Session> │
│ execute(handler, headers, uri) → CF<Session> │
└──────────────────────────┬──────────────────────────────┘
▲ 추상 베이스 ▲ SockJS 폴백 구현
┌──────────┴───────────┐ ┌────────────┴───────────────┐
│ AbstractWebSocketClient│ │ SockJsClient │
│ └ StandardWebSocket │ │ List<Transport> │
│ Client (Jakarta) │ │ ├ WebSocketTransport │
│ └ (Jetty 등) │ │ └ XhrTransport(RestClient)│
└────────────────────────┘ └─────────────────────────────┘
▲ 사용
┌──────────┴───────────────────────────┐
│ ConnectionManagerSupport │ (SmartLifecycle)
│ start()=connect stop()=disconnect │
│ └ WebSocketConnectionManager │
└────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ WebSocketStompClient (extends StompClientSupport) │
│ WebSocketClient를 감싸 STOMP over WebSocket 제공 │
└─────────────────────────────────────────────────────────┘
WebSocketClient는 "주어진 URL로 핸드셰이크를 보내고, 맺어진 세션을 핸들러로 처리"하는 계약이다. 두 execute 오버로드 모두 CompletableFuture<WebSocketSession>을 돌려준다 — 6.0부터 비동기 우선이다.
CompletableFuture<WebSocketSession> execute(WebSocketHandler webSocketHandler,
String uriTemplate, @Nullable Object... uriVariables);
CompletableFuture<WebSocketSession> execute(WebSocketHandler webSocketHandler,
@Nullable WebSocketHttpHeaders headers, URI uri);핸들러는 서버 측과 동일한 WebSocketHandler다. 연결이 열리면 afterConnectionEstablished, 메시지가 오면 handleMessage가 똑같이 호출된다.
AbstractWebSocketClient는 URI 확장과 헤더 정제 같은 공통 작업을 처리한 뒤 서브클래스의 실제 연결 메서드로 넘긴다.
public final CompletableFuture<WebSocketSession> execute(WebSocketHandler handler,
WebSocketHttpHeaders headers, URI uri) {
assertUri(uri);
HttpHeaders headersToUse = new HttpHeaders();
if (headers != null) {
headers.forEach((header, values) -> {
if (!specialHeaders.contains(header.toLowerCase(...))) // 핸드셰이크 전용 헤더 제외
headersToUse.put(header, values);
});
}
... // doHandshakeInternal(...) 로 위임 (서브클래스 구현)
}specialHeaders(sec-websocket-key, connection, upgrade, host 등)는 런타임이 직접 세팅하므로 사용자 헤더에서 걸러낸다. 기본 구현 StandardWebSocketClient(client.standard)는 Jakarta WebSocket API의 WebSocketContainer.connectToServer(...)로 연결한다. Jetty용 별도 구현도 있다.
매번 execute를 호출하기보다, 애플리케이션이 뜰 때 미리 정한 서버에 자동 접속하고 내려갈 때 끊고 싶을 때가 많다. ConnectionManagerSupport가 이를 SmartLifecycle로 자동화한다.
public abstract class ConnectionManagerSupport implements SmartLifecycle {
// start() → openConnection() / stop() → closeConnection()
// setAutoStartup(true)면 ApplicationContext refresh 시 자동 연결
}생성자에 URI(또는 URI 템플릿)를 받아 두고, 컨테이너 생명주기에 연결/해제를 건다. WebSocketConnectionManager는 이를 구체화해 WebSocketClient + WebSocketHandler를 들고 start() 시 client.execute(handler, uri)로 접속한다. 빈으로 등록하고 autoStartup을 켜면, 별도 코드 없이 "앱이 살아 있는 동안 유지되는 클라이언트 연결"이 된다.
03장의 SockJS가 서버 측이었다면, SockJsClient는 그 클라이언트 측으로 WebSocketClient를 구현한다. 여러 Transport(연결 시도 방법)를 들고, 가능한 것부터 차례로 시도해 WebSocket이 막히면 HTTP 기반으로 폴백한다.
public class SockJsClient implements WebSocketClient, Lifecycle {
private final List<Transport> transports; // WebSocketTransport, XhrTransport(RestClient/Jetty)
private InfoReceiver infoReceiver; // /info 조회로 서버 능력 파악
private final Map<URI, ServerInfo> serverInfoCache;
...
}연결 절차: (1) InfoReceiver로 서버의 /info를 조회해 WebSocket 지원 여부·엔트로피 등을 알아내고(결과는 serverInfoCache에 캐시), (2) 설정된 Transport 중 적합한 것을 골라 TransportRequest로 실제 연결을 시도한다. WebSocketTransport는 내부적으로 또 하나의 WebSocketClient를 쓰고, XhrTransport(RestClientXhrTransport 등)는 HTTP long-polling/streaming으로 SockJS 세션을 흉내 낸다. 지원 스킴은 ws/wss/http/https다. 서버와 마찬가지로 Jackson이 있으면 메시지 코덱을 자동 선택한다.
WebSocketStompClient는 spring-messaging의 StompClientSupport를 상속하고 SmartLifecycle을 구현해, STOMP over WebSocket 클라이언트를 제공한다. 생성자에 WebSocketClient(일반 StandardWebSocketClient 또는 SockJsClient)를 받아 그 위에 STOMP를 얹는다.
public class WebSocketStompClient extends StompClientSupport implements SmartLifecycle {
// connect(url, StompSessionHandler) → CompletableFuture<StompSession>
// 내부적으로 WebSocketClient.execute(...)로 연결 후
// 프레임 ↔ Message 인코딩/디코딩 (StompEncoder/StompDecoder, SplittingStompEncoder)
}서버 측 StompSubProtocolHandler(04장)와 대칭이다. 클라이언트는 connect로 STOMP 세션을 열고 StompSession/StompSessionHandler로 SUBSCRIBE/SEND를 수행하며, 큰 메시지 분할 인코딩과 heartbeat(TaskScheduler 필요)를 처리한다. SockJS WebSocketClient를 주입하면 클라이언트도 폴백을 갖춘 STOMP 통신이 된다.
[ApplicationContext refresh]
│ WebSocketStompClient (SmartLifecycle) 또는 WebSocketConnectionManager.start()
▼
WebSocketStompClient.connect(url, sessionHandler)
│
▼
(SockJsClient라면) InfoReceiver: GET /info → 서버 능력 파악
│ WebSocket 가능 → WebSocketTransport, 불가 → XhrTransport
▼
WebSocketClient.execute(handler, uri) → CompletableFuture<WebSocketSession>
│ 핸드셰이크 (서버의 02장 경로와 마주봄)
▼
WebSocketSession 열림 → WebSocketStompClient가 STOMP CONNECT 프레임 전송
│
▼
서버 CONNECTED 수신 → CompletableFuture<StompSession> 완료
│
▼
session.subscribe("/topic/...", handler) / session.send("/app/...", payload)
│ 프레임 ↔ Message 인코딩/디코딩
▼
[shutdown] stop() → STOMP DISCONNECT → 세션 종료
WebSocketClient.execute— 클라이언트 핸드셰이크를 시작하고CompletableFuture<WebSocketSession>을 반환. 핸들러는 서버와 동일한WebSocketHandler.AbstractWebSocketClient.execute— URI 확장과 핸드셰이크 전용 헤더 정제 후 서브클래스 연결로 위임.ConnectionManagerSupport.start/stop— 컨테이너 생명주기에 연결/해제를 자동 결합(autoStartup).SockJsClient연결 —/info조회 후Transport목록에서 적합한 전송을 골라 폴백 연결.WebSocketStompClient.connect—WebSocketClient위에 STOMP 세션을 수립.
- 방향만 반대, 추상은 공유: 클라이언트도
WebSocketHandler/WebSocketSession을 그대로 쓴다. 서버용으로 작성한 핸들러를 테스트나 서버 간 통신에 재사용 가능. - 선언적 vs 명령적: 일회성 연결은
execute, 앱 수명과 함께하는 연결은WebSocketConnectionManager(SmartLifecycle). - 클라이언트 폴백 대칭성:
SockJsClient가 서버 SockJS와 대칭을 이뤄, 폴백이 필요한 환경에서도 동일 API로 연결. - 계층화된 합성:
WebSocketStompClient→SockJsClient→WebSocketTransport→StandardWebSocketClient처럼 클라이언트들을 포개어, 폴백 갖춘 STOMP 통신을 조립.
클라이언트 측은 서버 측 추상(WebSocketHandler/WebSocketSession)을 방향만 바꿔 재사용한다. WebSocketClient가 핸드셰이크를 시작하고, WebSocketConnectionManager가 그것을 컨테이너 생명주기로 자동화하며, SockJsClient가 서버 SockJS와 대칭되는 클라이언트 폴백을 제공하고, WebSocketStompClient가 그 위에 STOMP를 얹는다. 이 클라이언트들을 포개면 "폴백을 갖춘 STOMP 클라이언트"까지 한 줄의 일관된 API로 구성된다.