WebSocket 서브프로토콜

강동현·2025년 4월 24일
0

Spring Websocket

목록 보기
5/9

Websocket은 단일 TCP 연결을 통해서 영구적인 양방향 통신 채널을 제공하는 컴퓨터 통신 프로토콜이다. 이는 실시간 웹 애플리케이션 구현 시 기존 HTTP의 요청/응답 패러다임이 가진 한계를 극복하기 위해서 설계되었다.

Websocket 연결의 시작

WebSocket 연결은 HTTP 핸드셰이크를 통해서 시작되고, 일단 연결되면 텍스트나 바이너리 데이터를 자유롭게 주고 받을 수 있다. 여기서 WebSocket 표준(RFC 6455)은 데이터 전송 방식만 정의할 뿐, 그 안에 담기는 메시지의 내용이나 의미에 대해서는 아무것도 규정하지 않는다. 이는 WebSocket을 범용적인 전송 계층으로 유지하려는 의도지만, 실제 애플리케이션 개발에서는 "그래서 이 데이터가 무슨 의미인데?"라는 의구심이 생길 것이다.

WebSocket의 숨겨진 조력자

단순한 바이트 스트림 교환만으로는 복잡한 애플리케이션 로직(채팅 메시지 구분, 발행/구독 패턴 구현 등)을 만들기 어렵다. 이러한 문제를 해결해주기 위해서 등장한 것이 WebSocket 서브프로토콜이다.

서브프로토콜이 뭐고 왜 필요해?

WebSocket 서브프로토콜은 기본 WebSocket 전송 계층 위에서 동작하는 애플리케이션 수준의 프로토콜이다. WebSocket 연결이 완료된 후 클라이언트와 서버 간에 교환되는 메시지의 형식, 구조, 의미, 그리고 상호작용 방식을 정의한다.

WebSocket이 "어떻게" 데이터 전송할지에 집중한다면, 서브프로토콜은 "무엇을", "어떤 의미로" 전송할지 규정하는 것이다.

  • 메시지 의미 부여 : 기본 WebSocket은 텍스트/바이너리 구분만 한다. 서브프로토콜은 "이건 채팅 메시지야", "이건 시스템 알림이야!" 처럼 의미를 명확하게 구분하게 해준다. 이게 없다면... 수신된 데이터를 해석하기 위해서 복잡한 로직을 양쪽에서 구현해야될 것이다.

  • 통신 구조화 : 서브 프로토콜은 발행/구독, 요청/응답, 메시지 큐잉과 같은 정형화된 통신 패턴을 제공한다. 예를 들어 STOMP은 Pub/Sub과 queuing을 WAMP는 Pub/Sub과 원격 프로시저 호출을 지원한다.

  • 상호 운용성 향상 : STOMP나 MQTT처럼 표준화된 서브프로토콜을 사용하면 서로 다른 기술 스택으로 만들어도 원활한 통신이 가능하다

  • 바퀴 재발명 방지 : 이미 잘 만들어진 서브프로토콜을 사용하면 커스텀으로 인해 생기는 오류 가능성은 생각 안해도 되고 안정성도 확보된다.

  • 프레임워크 통합 : Spring 과 같은 프레임워크는 STOMP와 같은 서브프로토콜을 기반으로 강력한 기능을 제공한다. @MessageMapping 같은 어노테이션으로 메시지 컨트롤러 메서드에 연결하는 편리함이 있다.

서브프로토콜 협상 과정 (RFC 6455)

어떤 서브프로토콜을 사용할지는 WebSocket 연결 수립 시 핸드셰이크 과정에서 명확하게 결정되는데 이 과정은 RFC 6455에 정의 되어 있다. 이는 HTTP 헤더를 통해서 이루어지며 먼저 클라이언트부터 확인해보자.

  • 클라이언트의 제안 : 클라이언트는 WebSocket 연결을 요청하는 HTTP Get 요청에 Sec-WebSocket-Protocol 헤더를 포함하여 자신이 지원하고 사용하고 싶은 서브 프로토콜 목록을 선호도 순으로 서버에 제안한다.
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: stomp, chat-v2  // stomp를 더 선호
Sec-WebSocket-Version: 13
  • 서버의 선택 및 응답 : 서버는 클라이언트가 제안한 목록을 보고 자신이 지원하며 사용하기로 결정한 하나의 서브프로토콜 이름을 응답 헤더의 Sec-WebSocket-Protocol 값으로 돌려준다. 만약 서버가 제안된 프로토콜을 지원하지 않거나 사용하지 않기로 했다면 이 헤더 자체를 응답에 포함하지 않는다. -> 빈값을 보낸다? 표준 위반이다.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: stomp // 클라이언트가 제안한 stomp를 선택
  • 클라이언트 검증 : 클라이언트는 서버의 응답을 보고 협상 결과를 확인한다.
  • 서버가 Sec-WebSocket-Protocol 헤더를 보냈다면 그 값이 자신이 제안했던 목록에 포함된 것인지 확인한다. 만약 목록에 없던 프로토콜이라면 연결을 실패
  • 헤더를 보내지 않으면, 서브프로토콜 없이 연결이 진행됨을 의미한다. 클라이언트는 서브프로토콜이 필수였다면 연결을 종료할 수 있다.

다양한 맛집 서브프로토콜

WebSocket 위에서 동작하는 여러 서브프로토콜이 있지만, 웹 개발에서 자주 마주치는 주요 프로토콜들은 다음과 같다.

  • STOMP
    - 특징 : 텍스트 기반, 프레임 구조(명령어, 헤더, 본문) 단순함, Pub/Sub 및 Point-to-Point지원, 메시지 확인 응답, 트랜잭션
    • 사용처 : 웹 메시지(채팅, 알림), 메시지 브로커 연동
    • IANA 식별자 : v10.stomp, v11.stomp, v12.stomp 등.
  • MQTT
    - 특징 : 경량 바이너리 프로토콜. Pub/Sub, Qos 레벨, 유언 기능 WebSocket은 터널링 역할.
    • 주 사용처 : 웹 기반 IoT 대시보드, 브라우저에서 MQTT 브로커 직접 통신
    • IANA 식별자 : mqtt
  • WAMP
    - 특징 : RPC와 Pub/Sub 통합 제공, 라우터 기반 아키텍처, URI 기반 리소스 식별.
    - 주 사용처 : 분산 시스템, 마이크로서비스 통신, RPC/PubSub 동시 필요 앱
    • IANA 식별자 : wamp
  • AMQP
    - 특징: 기능 풍부한 바이너리 프로토콜(주로 RabbitMQ 등 엔터프라이즈 브로커용), 복잡한 라우팅, 높은 신뢰성. WebSocket은 터널링 역할.
    - 주 사용처: 웹에서 AMQP 브로커 직접 연동, 엔터프라이즈 메시징 확장.
    - IANA 식별자: amqp (또는 AMQPWSB10).

어떤 서브프로토콜을 선택할지는 애플리케이션의 요구사항에 따라서 결정하면 된다.

Spring에서의 설정 (복습하기)

Spring Framework는 WebSocket, 특히 STOMP 기반 메시징을 위한 강력하고 편리한 기능을 제공한다. 앞서 포스팅에서 사용법을 알아본 것 처럼 @EnableWebSocketMessageBroker 와 WebSocketMessageBrokerConfigurer 를 사용하는 것이 핵심

  • 설정
@Configuration
@EnableWebSocketMessageBroker // WebSocket 메시지 브로커 활성화
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 1. 메시지 브로커 설정
        config.enableSimpleBroker("/topic", "/queue"); 
        // "/topic", "/queue" 프리픽스를 사용하는 SimpleBroker 활성화
        
        // config.enableStompBrokerRelay("/topic","/queue").setRelayHost("localhost")... 
        // 외부 브로커(RabbitMQ 등) 사용 시

        // 2. 애플리케이션 목적지 프리픽스 설정
        config.setApplicationDestinationPrefixes("/app"); 
        // "/app" 프리픽스로 시작하는 메시지는 @MessageMapping 메서드로 라우팅
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 3. STOMP 엔드포인트 등록
        registry.addEndpoint("/chat-websocket") // 클라이언트가 WebSocket 핸드셰이크를 위해 연결할 엔드포인트
                .setAllowedOrigins("*"); // CORS 허용 (실제 환경에서는 특정 Origin만 허용하는 것이 안전)
                // .withSockJS(); // WebSocket 미지원 브라우저를 위한 SockJS 폴백 활성화 시
    }
}
  • 메시지 처리
@Controller
public class GreetingController {

    @MessageMapping("/hello") // 클라이언트가 "/app/hello"로 메시지를 보내면 이 메서드가 처리
    @SendTo("/topic/greetings") // 메서드 반환값을 "/topic/greetings" 목적지로 브로드캐스팅
    public Greeting handleHello(HelloMessage message) throws Exception {
        // 메시지 처리 로직 (예: DB 저장, 다른 서비스 호출 등)
        Thread.sleep(1000); // 예시: 처리 시간 가정
        return new Greeting("안녕하세요, " + HtmlUtils.htmlEscape(message.getName()) + "!");
    }

    // HelloMessage, Greeting 클래스는 간단한 DTO (Data Transfer Object) 라고 가정
}

마무리

이렇게 WebSocket 서브프로토콜에 대해서 알아보게 되었는데 서브 프로토콜은 특별한 이유가 없는한 STOMP와 활용하는 것을 추천한다. 만약 극도의 성능이 요구되는 경우 WebSocket 핸들러를 직접 구현하는 방식을 고려할 수 있다.

profile
스스로에게 질문하고 답을 할 줄 아는 개발자

0개의 댓글