Skip to content

Latest commit

 

History

History
157 lines (123 loc) · 10.9 KB

File metadata and controls

157 lines (123 loc) · 10.9 KB

06. 클라이언트

무엇을 / 왜

지금까지는 서버가 연결을 "받는" 쪽이었다. 하지만 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 — 클라이언트 핸드셰이크 계약

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 — 공통 전처리

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용 별도 구현도 있다.

ConnectionManagerSupport / WebSocketConnectionManager — 선언적 연결

매번 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을 켜면, 별도 코드 없이 "앱이 살아 있는 동안 유지되는 클라이언트 연결"이 된다.

SockJsClient — 클라이언트 측 폴백

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 — STOMP 클라이언트

WebSocketStompClientspring-messagingStompClientSupport를 상속하고 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 통신이 된다.

동작 흐름 — 선언적 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.connectWebSocketClient 위에 STOMP 세션을 수립.

설계 포인트 / 확장점

  • 방향만 반대, 추상은 공유: 클라이언트도 WebSocketHandler/WebSocketSession을 그대로 쓴다. 서버용으로 작성한 핸들러를 테스트나 서버 간 통신에 재사용 가능.
  • 선언적 vs 명령적: 일회성 연결은 execute, 앱 수명과 함께하는 연결은 WebSocketConnectionManager(SmartLifecycle).
  • 클라이언트 폴백 대칭성: SockJsClient가 서버 SockJS와 대칭을 이뤄, 폴백이 필요한 환경에서도 동일 API로 연결.
  • 계층화된 합성: WebSocketStompClientSockJsClientWebSocketTransportStandardWebSocketClient처럼 클라이언트들을 포개어, 폴백 갖춘 STOMP 통신을 조립.

정리

클라이언트 측은 서버 측 추상(WebSocketHandler/WebSocketSession)을 방향만 바꿔 재사용한다. WebSocketClient가 핸드셰이크를 시작하고, WebSocketConnectionManager가 그것을 컨테이너 생명주기로 자동화하며, SockJsClient가 서버 SockJS와 대칭되는 클라이언트 폴백을 제공하고, WebSocketStompClient가 그 위에 STOMP를 얹는다. 이 클라이언트들을 포개면 "폴백을 갖춘 STOMP 클라이언트"까지 한 줄의 일관된 API로 구성된다.