웹소켓(WebSocket) 가볍게 알아보기 + STOMP 프로토콜

올해 초 종합 운세 웹 서비스 프로젝트를 진행했습니다.
저희 사이트에는 사용자의 사주 정보를 기반으로 대화 상대를 추천해 주고 채팅으로 연결해 주는 기능을 제공하고 있는데, 저를 포함한 두 명이 이 기능을 담당했었습니다.
채팅 기능을 구현하기 위해 웹소켓 프로토콜 통신을 이용했고, 그때의 경험과 기억을 살려 정리해두면 좋겠다 싶어서 글을 작성하게 되었습니다.
사실 자잘한 버그들이 조금 있기도 하고, 추가해야 할 기능들이 있어서 아직 홍보는 따로 안하고 있지만, 기능이 궁금하신 분들은 https://www.unsemawang.com 접속하셔서 이용해보실 수 있습니다.
# 00. 기존 실시간 양방향 통신 방법
웹 애플리케이션에서 클라이언트⇄서버 간 실시간 양방향 통신을 구현할 때, 전통적으로 많이 사용해 온 방식은 폴링(polling)입니다.
클라이언트가 일정 간격으로 서버에 HTTP 요청을 보내 최신 데이터를 가져오는 방식인데, 폴링은 다음과 같은 한계가 있습니다.
폴링의 한계
1. 서버의 TCP 연결 부담
클라이언트 한 명당 “업데이트 요청용” 커넥션 ( 주로 Short-Polling의 경우입니다 ) 혹은 “푸시 대기용” 커넥션 ( Long-Polling과 Streaming Connection의 경우입니다 ) 을 별도로 관리해야 하기 때문에, 실제 운영 시에는 다수의 TCP 소켓을 유지하게 됩니다.
2. 높은 전송 오버헤드
매번 HTTP 헤더를 주고받기 때문에, 전송 효율이 떨어지고 레이턴시가 증가합니다.
3. 클라이언트 측 매핑 로직 필요
여러 연결에서 돌아오는 응답을 각 요청과 매핑해 주지 않으면, 어떤 요청에 대한 응답인지 추적하기 어렵습니다。
이와 같은 한계를 극복하기 위해 실시간 양방향 통신에 대해 WebSocket 프로토콜을 이용하게 되었습니다。
# 01. WebSocket
WebSocket?
웹소켓은 HTML5 표준으로 제안된 완전 양방향 (full-duplex) 통신 프로토콜입니다。
웹소켓의 특징은 다음과 같습니다。
웹소켓 특징
1. 단일 TCP 연결 유지
핸드셰이크 이후에는 클라이언트와 서버가 동일한 소켓 하나만 사용하기 때문에 커넥션 수가 고정됩니다。
2. 경량화된 프레임 전송
웹소켓은 최소 2바이트 크기의 프레임 헤더만 붙여 메세지를 교환하기 때문에 HTTP 대비 오버헤드가 극도로 낮습니다。
3. 양방향 이벤트 API
웹소켓은 하나의 지속적인 연결 위에서 send()와 onmessage 이벤트 핸들러만으로 통신합니다。
때문에 응답 추적을 위해 여러 연결을 구분 및 매핑할 필요가 없습니다。
웹소켓의 이러한 특징은 기존 폴링 방식의 한계를 극복하는 모습을 보여줍니다。
WebSocket 핸드셰이크

웹소켓 연결을 위해 최초에 클라이언트는 서버로 HTTP 요청을 보냅니다。
이때 헤더에 Connection: Upgrade 값을 담아 이 커넥션에 대해 업그레이드를 시도하겠다는 의미를 전달하고, Upgrade: websocket 값을 담아 웹소켓 프로토콜로 업그레이드하겠다는 의도를 명시합니다。
또한 Sec-WebSocket-Key에 클라이언트가 생성한 16바이트 난수를 Base64로 인코딩하여 담습니다。
서버에서는 웹소켓 연결로 업그레이드 하는 것을 동의하는 의미로 HTTP 헤더에 Connection: Upgrade와 Upgrade: websocket 값을 그대로 담아 보내며, 클라이언트로부터 받은 키 값에 고정 GUID를 덧붙여 SHA-1이라는 방식의 해시 처리 이후 Base64로 인코딩한 값을 Sec-WebSocket-Accept로 돌려줍니다。
클라이언트에서는 서버로부터 받은 값을 검증하고, 이후 TCP 위에 완전 양방향 채널이 열리게 됩니다。
웹소켓 연결을 끊을 때는 클라이언트 혹은 서버 한 측에서 Close 프레임을 전송합니다。
일반적으로 Close 프레임을 받은 쪽은 지체 없이 Close 프레임으로 응답하며, 양쪽이 Close 프레임을 주고받으면 웹소켓 계층에서 연결이 완전히 종료된 것으로 간주하고 TCP 연결도 닫습니다。
# 02. STOMP 프로토콜
STOMP?
STOMP ( Simple Text Oriented Messaging Protocol ) 는 텍스트 기반의 경량 메시징 서브 프로토콜로, 주로 웹소켓 위에 얹어 사용됩니다。
웹소켓 프로토콜만으로도 완전 양방향 통신이 가능한데 서브 프로토콜을 사용하는 이유가 무엇일까요?
기본적으로 STOMP는 별도의 메시징 포맷이나 파싱 로직을 따로 정의할 필요 없이, 표준 STOMP 프레임을 사용해 통신을 구현할 수 있습니다。
만약 STOMP를 사용하지 않는다면, 서버에서 보낸 그대로의 값만 받겠지만, STOMP를 사용함으로써 커맨드, 헤더, 바디의 형태로 데이터를 받을 수 있습니다。
또한 RabbitMQ와 같은 메세지 브로커를 연결하여 PUB/SUB 패턴을 지원하고, 스프링 시큐리티 및 트랜잭션과의 연동도 간편하게 적용할 수 있습니다。
스프링에서는 웹소켓 위에 STOMP를 얹는 기능을 제공하고 있기 때문에 저희 역시 채팅 기능 구현 당시 STOMP 프로토콜을 사용했습니다。
PUB/SUB
STOMP가 지원하는 PUB/SUB 패턴은 발행(publish)과 구독(subscribe)의 개념으로 메세지를 처리하는 패턴입니다。
발행자(publisher)는 목적지로 메세지를 전송하며 브로커는 수신한 메세지를 목적지를 구독한 구독자(subscriber)들에게 프레임으로 전달합니다。
해당 패턴에서는 발행자와 구독자의 역할이 개념상으로 분리되어 있지만, 실제 구현에서는 한 엔티티가 동시에 두 역할을 모두 수행할 수 있습니다。

웹소켓 프로토콜에 대해 간략하게 훑어보는 느낌으로 글을 작성해봤습니다.
기회가 되면 스프링에서 웹소켓 및 STOMP 프로토콜로 통신을 하는 구체적인 방법까지도 글을 작성해보도록 하겠습니다.