회사 업무중 모처럼 새로운 기술을 사용하는 프로젝트에 참여했다. 브라우져에서 실시간으로 알림을 받을 수 있는 기능을 개발하는 일이다. 사용자에게 알림톡으로 보내던 메세지를 사내 메시징 시스템으로 전환하는 것이 목표다. 카톡이나 문자 메세지로 알림을 확인하던 고객은 웹서비스 안에서 바로 알림을 받을 수 있게되었다.
웹 브라워저로 실시간 메세지를 받으려면 몇 가지 기술이 필요했다. 그중 스톰프(Stomp)라고 불리는 메시징 프로토콜에 대해서 정리해 보겠다.
스톰프(Stomp)
간단한 혹은 스트리밍 문자 지향 메세지 프로토콜이다.
STOMP is the Simple (or Streaming) Text Orientated Messaging Protocol.
텍스트 기반으로 메세지를 주고 받을 수 있다. 문자가 크다면 스트리밍 될 수도 있다고 한다. 여기까지는 좀 모호하다. 더 읽어보자.
스톰프는 상호운용할 수 있는 형식을 제공한다. 클라이언트는 언어, 플랫폼, 브로커와 무관하게 메세지를 교환할 수 있다.
STOMP provides an interoperable wire format so that STOMP clients can communicate with any STOMP message broker to provide easy and widespread messaging interoperability among many languages, platforms and brokers.
문장이 어렵지만 쉽게 정리해 보자. 언어와 플랫폼에 국한되지 않고 다양하게 사용할 수 있다. 그러한 이유는 공통으로 사용할 수 있는 메세지 전송 형식이 있기 때문이다. 전송 주체인 클라이언트와 브로커는 서로 약속한 포맷으로 데이터를 주고 받는 것 같다. 서버라고 하지 않고 브로커라고 표현한 것은 받은 메세지를 전달하는 역할을 하기 때문일 것이다. 클라이언트 A는 브로커를 통해 클라이언트 B에게 메세지를 전송한다.
웹 소켓
스톰프 명세는 웹소켓과 무관하다. 하지만 프로젝트에서 사용한 기술은 웹소켓과 스톰프를 같이 사용한 기술이었다. 지금도 여전히 헷갈리는 부분은 스톰프와 웹소켓의 관계다. 브라우져에서 쓰려고 웹소켓을 사용해 구현한 것이라고 추측한다. 웹소켓을 처음 다루기 때문에 간단히 정리하고 넘어가자.
웹소켓은 HTTP처럼 TCP계층 위에서 동작하는 어플리케이션 계층의 프로토콜이다. 단방향 HTTP와 양방향 소켓이 다른 것처럼 웹소켓도 양방향 통신을 한다. 웹소켓을 사용하면 브라우져와 서버가 항상 연결되어 있기 때문에 서로 언제든지 메세지를 주고 받을 수 있다. 아파트 관리사무소 방송은 듣기만 하지만 직접 찾아가면 대화를 주고 받을 수 있는 것 처럼.
웹 소켓 실습
웹 소켓 동작을 확인하기 위해 간단히 서버를 만들고 연결해 보자.
// server.js
// 웹소켓 구현체인 ws 라이브러리를 사용한다.
const { WebSocketServer } = require("ws")
// 웹 소켓 서버를 만든다. 4000 번 포트를 사용한다.
const wss = new WebSocketServer({
port: 4000,
})
// 클라이언트와 연결되면 콜백이 실행된다.
wss.on("connection", ws => {
// 클라이언트에게 '안녕' 메제지를 보낼 것이다.
ws.send("안녕")
// 연결된 클라이언트로 부터 메세지를 받으면 콜백이 실행된다.
ws.on("message", data => {
// 받은 메세지를 다시 클라이언트로 전달한다. 마치 에코서버 같다.
ws.send(data)
console.log(data.toString("utf-8"))
})
})
웹소켓 구현체인 ws 라이브러리를 사용했다. 이전에 socket.io라고 기억하는데 이제는 이걸 많이 사용하는 것 같다. 별 생각없이 ws를 사용했는데 간편하게 웹소켓 서버를 만들수 있었다.
사용할 포트를 지정하고 웹 소켓 서버를 만들었다. 클라이언트가 요청하면 connection 이벤트가 발생하는데 이 때 실행할 콜백함수도 등록했다. 연결된 클라이언트에게 '안녕' 메세지를 보낼것이다. 클라이언트가 메세지를 보내면 그대로 응답하는 에코 서버를 만들었다.
이제 실행하자.
node server.js
클라이언트는 nc를 사용할 것이다. netcat이란 명령어인데 서버와 연결해 텍스트로 메세지를 서로 주고 받을 수 있는 프로그램이다. 윈도우즈의 텔렛과 비슷하다. 맥에서는 기본 명령어다.
nc localhost 4000
GET /?encoding=text HTTP/1.1
Connection: Upgrade
Host: 127.0.0.1
Upgrade: websocket
Sec-WebSocket-Key: P7Kp2hTLNRPFMGLxPV47eQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fX+lgbZ/+V4eiY0s9Muig1sv880=
안녕
클라이언트와 서버간 웹소켓 연결을 위한 핸드쉐이킹 프레임이다. HTTP 프레임과 비슷하다. 첫 줄에 메소드, 주소, 프로토콜을 입력한다. 두번째 줄 부터는 키/값으로 이뤄진 헤더다. 이 프레임을 전달하고 서버로부터 '안녕' 메세지를 받았다.
주고 받은 헤더를 보면 웹소켓으로 변경 요청을 했고 서버는 프로토콜 스위칭을 했다. 이후부터는 웹소켓 프로토콜로 통신해야하는데 nc로는 흉내낼 수 없었다. 클라이언트에서 메세지 전송은 크롬 브라우져의 개발자 도구에서 직접 WebScoket 객체를 만들어 테스트할 수 있다.
const ws = new WebSocket("ws://localhost:4000")
ws.send("안녕하세요")
서버는 받은 메세지를 콘솔에 출력할 것이다.
스톰프와 웹소켓
스톰프는 프레임 단위로 메세지를 주고 받는다. 프레임은 커맨드, 헤더, 바디로 구성된 텍스트다.
- 커맨드
- 헤더
- 바디
클라이언트는 두 가지 유형으로 구분한다. 생상자와 소비자. 생산자는 말하고 싶어하는 클라이언트다. 말은 프레임이라는 형식으로 만들어 전달된다. 이것을 서버가 받아 주는데 브로커라고 부른다. 다른 클라이언트에게 전달하는 역할을 하기 때문이다. 소비자는 말을 듣고 싶어하는 클라이언트다. 브로커는 소비자에게 프레임을 전달할 것이다.
각 클라이언트는 역할에 맞는 커맨드를 이용해 프레임을 만드다.
- 생산자: CONNECT, SEND frame
- 소비자: CONNECT, SUBSCRIBE frames
스톰프 실습
간단히 스톰프 프레임을 주고 받는 실습을 해보자. 스톰프 브로커 중 웹소켓을 지원하는 Activemq를 설치하자.
# 서버 프로그램을 설치한다.
brew install apache-activemq
# 서버를 실행한다. console은 서버 로그를 남기는 옵션이다.
activemq console
apache-activemq를 설치하고 실행했다. 부명령어 console을 사용하면 서버를 포그라운드(foreground)로 실행해 로그를 확인 할수 있다. 기본 61613 포트를 사용한다.
이 서버에 연결해 보자.
nc 127.0.0.1 61613
핸드 쉐이킹 과정을 마치면 텍스트를 받을 준비가 되었다. 서버와 연결하기 위한 프레임을 전송해 보자.
CONNECT
^@
헤더와 바디없이 커맨드만 사용해서 CONNECT 프레임을 만들었다. 마지막에는 한 줄 추가한 뒤 컨트롤 + @ 문자를 입력하면 하나의 프레임이 종료되었다는 의미다. 여기서 문자 ^가 아니라 키보드의 컨트롤을 입력해야 한다는 것을 주의하자. 컨트롤 + @를 동시에 입력한다.
CONNECTED
server:ActiveMQ/5.17.2
heart-beat:0,0
session:ID:gimjeonanuiiMac.kornet-51546-1667078224991-3:1
version:1.0
엔터키를 입력하고 서버로부터 연결되었다는 프레임을 받았다.
이 클라이언트를 소비자 역할로 삼자. 스톰프는 토픽을 구독하는 형태인데 생산자가 토픽에 프레임를 전송하면 브로커는 구독하고 있는 소비자에게 프레임을 전달하는 구조다. 마치 바리스타가 커피를 준비하고 완료하면 고객이 들고 있는 진동벨을 울리는 것 처럼.
SUBSCRIBE
destination:/queue/foo
^@
SUBSCRIBE 커맨드를 사용했다. 헤더에는 목적지를 의미하는 destination 키와 구독할 토픽 /queue/foo 값을 사용해서 헤더를 구성했다. 마지막 문자를 입력하고 프레임을 전송하면 토픽을 구독한 것이다.
이번에는 생산자 역할을 할 클라이언트를 만들자.
nc 127.0.0.1 61613
CONNECT
토픽으로 메세지를 전달할 수 있는 프레임을 만들자.
SEND
destination:/queue/foo
안녕
^@
SEND 커맨드를 사용했다. 헤더에는 메세지를 전달할 토픽을 지정했다. 한 줄 띄고 바디에 전달할 메세지 "안녕"을 입력했다. 마지막 문자를 입력하고 프레임을 전달한다.
소비자 클라이언트를 확인하면 생산자가 전달한 "안녕" 문자가 전달 되어 콘솔 기록되었을 것이다.
브라우저에서 사용하기
자바스크립트 라이브러리 구현체는 stomp.js를 먼저 사용했는데 더 이상 지원하지 않는 프로젝트였다. 아이가 태어난 후부터 더 이상 프로젝트에 쏟을 시간이 없다고 한다. 공감도 되고 은근히 응원하는 마음도 생겼다.
대신 이 프로젝트를 기초로해서 다시 만든 것이 @stomp/stompjs 다. 마지막 커밋이 작년 9월이라서 운영하는 프로젝트인가 의문이긴하지만. 어쩌겠는가? 내가 만드는 것보다는 사용하는게 현명한 일이다.
라이브러리 사용은 간단하다. 서버에 연결하고 메세지를 구독하는 코드다.
// 스톰프 클라이언트 객체를 만든다.
const client = new StompJs.Client({ brokerUrl, onConnect })
// 연결한다. CONNECT 프레임을 보낸다.
client.activte()
// 서버와 연결되었을 때 실행할 콜백함수
function onConnect() {
// 구독한다. SUBSCRIBE 커맨드를 보내다.
client.subscribe(topic, onMessage)
}
// 프레임을 받으면 실행할 콜백함수
function onMessage(message) {
console.log(message)
}
브로커 주소를 준비해 스톰프 클라이언트 객체를 만든다. activeate() 메소드로 CONNECT 프레임을 브로커로 전송한다. 서버와 연결되면 onConnect() 함수가 실행될 것이다. 곧장 토픽을 구독하기 위해 subscribe() 메소드를 사용해 SUBSCRIBE 프레임을 보낸다. 메세지를 받으면 onMessage() 함수가 실행 될 것이다.
직접 프레임을 보내는 방식과 유사하다. 라이브러리는 프레임에 해당하는 메소드를 제공하기 때문에 스톰프 스펙에 맞게 사용하면 어렵지 않게 스톰프 클라이언트를 만들 수 있었다.
결론
웹소켓을 사용해서 양방향 통신으로 메세지를 실시간으로 받는 기능을 만든다고 생각했다. 그래서 웹소켓을 준비하고 있었는데 막상 서버 스펙을 보고 스톰프라는 것이 나와서 좀 헤맸다. 스톰프라는 메시징 프로토콜이 있고 이것을 브라우져에서 사용하기 위해 웹소켓을 지원한 서버, 클라이언트 구현체가 있다.