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 핸들러를 직접 구현하는 방식을 고려할 수 있다.