새소식

framework/spring

[Spring] Web-Socket, SockJS, STOMP 이론

  • -

WebSocket, SockJS, STOMP 소개

WebSocket

WebSocket은 기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜이다.

웹 소켓은 HTTP를 사용하는 네트워크 데이터 통신의 단점을 보완하는데 그 목적이 있다.
웹 소켓을 설명하기 이전에, 웹 소켓의 등장 이전에는 HTTP 통신의 단점을 어떻게 해결하려고 했는지 알아보겠다.

WebSocket 등장 이전

모든 HTTP를 사용한 통신은 클라이언트가 먼저 요청을 보내고, 그 요청에 따라 웹 서버가 응답하는 형태이며 웹 서버는 응답을 보낸 후 웹 브라우저와의 연결을 끊는다.
이러한 통신 방식을 반이중 통신(Half Duplex)라고 한다.

실시간 검색어와 같이 서버에서 제공하는 데이터를 항상 최신으로 유지하고 싶다고 하자.
반이중 통식 방식을 지원하는 기존 HTTP 통신으로는 어떻게 해결할 수 있었을까?

Polling
클라이언트가 일반적인 HTTP 요청을 주기적으로 보내서 변경된 데이터를 확인하는 방식을 Polling이라고 한다.
가장 간단하고 쉬운 방식이지만 클라이언트가 주기적으로 요청을 보내기 때문에 클라이언트의 수가 많아지면 서버의 부담이 많아지게 된다. HTTP 커넥션을 맺고 끊는 것 또한 리소스를 많이 잡아먹는다는 문제가 있었다.

Polling의 문제를 해결하기 위해 Long-Polling이 등장한다.

Long-Polling
Long-Polling은 클라이언트에서 서버로 일단 HTTP 요청을 보내고, 서버는 요청을 받은 상태를 유지하다가 클라이언트가 원하는 이벤트가 서버에 발생하면 그 때 응답 메시지를 보낸다. 응답을 받은 클라이언트는 다시 서버로 요청을 보내 다음 이벤트를 기다리게 된다.
Polling 방식보다는 서버의 부담이 줄지만, 이 방식 역시 클라이언트로 보내는 이벤트의 발생 주기가 짧다면 Polling 방식과 별반 다를게 없다. 또한 동시에 여러 이벤트가 발생하게 되어 다수의 클라이언트에게 보내야 할 경우, 서버의 부담이 급증하게 된다는 문제가 있다.

이 때, SSE가 등장한다.

SSE(Server-Sent Event)
클라이언트가 서버로 HTTP 요청을 보내면 서버는 이벤트에 대한 응답을 할 때 이 HTTP 요청에 대한 연결을 끊지 않고 계속 유지한다. 이렇게 되면 서버는 이벤트가 발생하여 변경이 필요한 데이터만 계속 응답하면 된다. 이러한 방법을 SSE라고 한다.
클라이언트는 한 번만 요청을 보내놓으면 커넥션이 끊기지 않는 한 계속해서 변경된 데이터를 받을 수 있고, 서버 입장에서도 이벤트가 발생할 때 마다 HTTP 커넥션을 생성하지 않아도 되어 부담이 줄어들게 된다.

Polling, Long-Polling, SSE와 같은 방식은 실시간으로 변경된 데이터를 전달받을 수 있지만, 여전히 서버에서 클라이언트 방향으로, 단방향으로 데이터를 전송한다.
실시간 검색어나 주식, 날씨 정보와 같은 데이터는 양방향 통신이 필요없다. 서버의 데이터가 변동되면 단방향으로 데이터를 전달받으면 된다.

하지만 채팅의 경우에는 어떤가?
상대에게 실시간으로 메시지를 전송해야 하고, 상대의 메시지를 실시간으로 전달받아야 한다. 즉, 양방향 통신이 필요하다.
이렇게 서버를 통해 여러 클라이언트가 실시간으로 데이터를 주고 받기 위해 WebSocket 기술이 등장하게 된 것이다.

웹 소켓

위에서 언급했듯이, WebSocket은 기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜로, 서버와 클라이언트 사이에 Socket 커넥션을 유지하면서 양방향 통신을 가능하게 하는 기술이다.

웹 소켓의 동작 과정은 다음과 같다.

  1. 클라이언트가 HTTP 프로토콜로 Handshake 요청을 한다.
  2. 이에 대해 서버는 HTTP 상태 코드 101을 응답해준다. 이때 HTTP Upgrade 헤더를 사용하여 웹 소켓 프로토콜로 변경할 수 있도록 명시한다.
  3. 이후 통신 프로토콜을 WebSocket 프로토콜로 변환하여 데이터를 전송할 수 있게끔 한다.

WebSocket은 일반 Socket 통신과는 달리 HTTP 80, HTTPS 443 포트 위에서 동작하도록 설계되었다. 따라서 별도의 포트를 열지 않아도 된다.

HTTP 통신은 HTTP와 HTTPS로 통신하는데, HTTPS는 통신의 보안을 위해 HTTP에 보안을 적용한 것이다.
웹 소켓은 통신을 위해 ws를 사용하는데 ws는 HTTP에 대응하는 것이며, 보안을 위해서는 wss를 사용하여 통신해야 한다.

SockJS

웹소켓은 HTML5 이후에 나왔기 때문에 HTML5 이전의 기술로 구현된 서비스에서는 웹소켓 기술을 사용할 수 없다.
HTML5 이전의 기술로 구현된 서비스에서 웹소켓처럼 동작할 수 있도록 도와주는 것이 SockJS이다.

SockJS는 네이티브 웹소켓을 사용하려고 하는 WebSocket 클라이언트이며, 웹소켓을 지원하지 않는 구형 브라우저에 대체 옵션을 제공한다.
우선 WebSocket 연결을 시도하고 실패할 경우 SSE, Long-Polling과 같은 HTTP 기반의 다른 기술로 전환하여 다시 연결을 시도한다.

SockJS를 사용하면 애플리케이션이 웹소켓을 사용하도록 허용하고 브라우저 등에서 웹소켓을 지원하지 않는 경우에 웹소켓을 대체할 대안 기술을 사용하도록 한다. 런타임에 사용 기술을 변경하기 때문에 애플리케이션 코드의 변경이 필요없어 유연하다는 장점이 있다.

SockJS는 서버에서 주기적으로 Heartbeat 메시지를 전송하도록 하여, 프록시가 커넥션이 끊겼다는 결론을 내리지 않도록 한다. 주기적으로 보내는 시간을 heartbeat interval이라고 한다.
서버가 Heartbeat 메시지를 전송하고 클라이언트가 이에 대한 응답을 하지 않으면, 서버는 클라이언트가 죽은 것으로 판단한다.
반면 클라이언트는 마지막 Hearbeat 메시지를 전송받고 특정 시간(heartbeat timeout)동안 Heartbeat 메시지를 받지 못하면 서버가 죽은 것으로 판단하고 접속 종료와 재접속 흐름을 진행한다.
기본값으로 heartbeat interval은 25초, heartbeat timeout은 60초로 설정되며 설정을 변경할 수 있다.

(참고) STOMP를 이용해 Heartbeat를 주고 받게되면, SockJS의 Heartbeat 설정은 비활성화된다.

STOMP

웹소켓만을 이용하게 되면 해당 메시지가 어떤 요청인지, 어떻게 처리해줘야 하는지 직접 구현해야 한다. 세션을 관리하는 방법 또한 서버에서 직접 구현하게 된다.

STOMP는 Simple Text Oriented Message Protocol의 약자로, 메시지 전송을 효율적으로 하기 위한 프로토콜이다.
클라이언트와 서버가 전송할 메시지의 유형, 형식, 내용들을 정의하는 매커니즘으로, 메시지의 헤더에 값을 줘서 인증 처리를 구현하는 것도 가능하다.

STOMP는 PUB/SUB 구조로 동작한다.
PUB/SUB 구조는 메시지를 공급하는 주체와, 소비하는 주체를 분리하여 제공하는 메시징 방법이다.
따라서 메시지 송신, 수신에 대한 처리를 명확하게 정의할 수 있다.

STOMP 공식 문서

PUB/SUB 구조

PUB/SUB 구조에 대해 조금 더 설명해 보겠다.

PUB은 클라이언트의 메시지 송신과 관련된다.
서버가 /pub/chat-room/{chat-room-id}과 같은 메시지 요청 경로를 오픈했다고 하자.
클라이언트들은 서버에서 오픈한 메시지 요청 경로로 메시지를 전송하고, 서버는 메시지를 전달받아 적절한 처리를 한다.

SUB은 클라이언트의 메시지 수신과 관련된다.
클라이언트가 /sub/chat-room/{chat-room-id}와 같이 구독요청을 보내면, 서버는 해당 경로를 topic으로 관리하고 특정 이벤트가 발생했을 때 특정 topic을 구독중인 클라이언트들에게 메시지를 전달할 수 있다.
간단하게 말하면 topic을 기준으로 그룹을 나누고, 서버는 topic 그룹으로 세션을 관리, 클라이언트는 구독한 topic에 대한 메시지를 수신할 수 있도록 한다.

RabbitMQ, Kafka와 같은 외부 메시지 큐를 사용하는 이유
WebSocket만을 이용하게 되면, 세션 단위의 메시지 전달만 가능하다. 따라서 채팅방을 여러개 만들지 못한다는 문제가 있다.
이를 해결하기 위해 STOMP를 이용하여 pub/sub 구조로 여러 방을 만들고 STOMP Broker를 통해 특정 Topic을 구독중인 클라이언트들에게 메시지를 전달할 수 있다.
하지만 STOMP는 서버 메모리를 Broker로 사용하기 때문에 서버가 2개 이상일 경우, 한 서버에서 발생한 메시지를 다른 서버에 전달하기가 어렵다. 클러스터링을 통해 해결해야 한다.
이러한 문제를 해결하기 위해 외부 메시지 큐를 사용할 수 있다. 한 서버에서 발생한 메시지는 외부 메시지 큐에 적재되고, 외부 메시지 큐를 구독중인 모든 서버는 적재된 메시지를 가져와 메시지가 해당되는 토픽을 구독중인 모든 클라이언트들에게 전달할 수 있게 된다.

[Spring] Web-Socket, SockJS, STOMP 이론

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.