Websocket은 단일 TCP 연결을 통해서 영구적인 양방향 통신 채널을 제공하는 컴퓨터 통신 프로토콜이다. 이는 실시간 웹 애플리케이션 구현 시 기존 HTTP의 요청/응답 패러다임이 가진 한계를 극복하기 위해서 설계되었다.
WebSocket 연결은 HTTP 핸드셰이크를 통해서 시작되고, 일단 연결되면 텍스트나 바이너리 데이터를 자유롭게 주고 받을 수 있다. 여기서 WebSocket 표준(RFC 6455)은 데이터 전송 방식만 정의할 뿐, 그 안에 담기는 메시지의 내용이나 의미에 대해서는 아무것도 규정하지 않는다. 이는 WebSocket을 범용적인 전송 계층으로 유지하려는 의도지만, 실제 애플리케이션 개발에서는 "그래서 이 데이터가 무슨 의미인데?"라는 의구심이 생길 것이다.
단순한 바이트 스트림 교환만으로는 복잡한 애플리케이션 로직(채팅 메시지 구분, 발행/구독 패턴 구현 등)을 만들기 어렵다. 이러한 문제를 해결해주기 위해서 등장한 것이 WebSocket 서브프로토콜이다.
WebSocket 서브프로토콜은 기본 WebSocket 전송 계층 위에서 동작하는 애플리케이션 수준의 프로토콜이다. WebSocket 연결이 완료된 후 클라이언트와 서버 간에 교환되는 메시지의 형식, 구조, 의미, 그리고 상호작용 방식을 정의한다.
WebSocket이 "어떻게" 데이터 전송할지에 집중한다면, 서브프로토콜은 "무엇을", "어떤 의미로" 전송할지 규정하는 것이다.
메시지 의미 부여 : 기본 WebSocket은 텍스트/바이너리 구분만 한다. 서브프로토콜은 "이건 채팅 메시지야", "이건 시스템 알림이야!" 처럼 의미를 명확하게 구분하게 해준다. 이게 없다면... 수신된 데이터를 해석하기 위해서 복잡한 로직을 양쪽에서 구현해야될 것이다.
통신 구조화 : 서브 프로토콜은 발행/구독, 요청/응답, 메시지 큐잉과 같은 정형화된 통신 패턴을 제공한다. 예를 들어 STOMP은 Pub/Sub과 queuing을 WAMP는 Pub/Sub과 원격 프로시저 호출을 지원한다.
상호 운용성 향상 : STOMP나 MQTT처럼 표준화된 서브프로토콜을 사용하면 서로 다른 기술 스택으로 만들어도 원활한 통신이 가능하다
바퀴 재발명 방지 : 이미 잘 만들어진 서브프로토콜을 사용하면 커스텀으로 인해 생기는 오류 가능성은 생각 안해도 되고 안정성도 확보된다.
프레임워크 통합 : Spring 과 같은 프레임워크는 STOMP와 같은 서브프로토콜을 기반으로 강력한 기능을 제공한다. @MessageMapping 같은 어노테이션으로 메시지 컨트롤러 메서드에 연결하는 편리함이 있다.
어떤 서브프로토콜을 사용할지는 WebSocket 연결 수립 시 핸드셰이크 과정에서 명확하게 결정되는데 이 과정은 RFC 6455에 정의 되어 있다. 이는 HTTP 헤더를 통해서 이루어지며 먼저 클라이언트부터 확인해보자.
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 위에서 동작하는 여러 서브프로토콜이 있지만, 웹 개발에서 자주 마주치는 주요 프로토콜들은 다음과 같다.
v10.stomp
, v11.stomp
, v12.stomp
등.어떤 서브프로토콜을 선택할지는 애플리케이션의 요구사항에 따라서 결정하면 된다.
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 핸들러를 직접 구현하는 방식을 고려할 수 있다.