본문 바로가기

포트폴리오/웹 페이지 기반 오픈 채팅 서비스 (web-talk)

[web-talk] 채팅서버 구현시 STOMP를 써야하는 지 에대한 고찰

목차
들어가며
Web Socket이란?
STOMP란?
무엇을 써야하는가

 

 

 

들어가며

 새로운 사이드 프로젝트로 웹소켓 기반 채팅 서비스를 구현해보았다. 해당 프로젝트를 진행하면서 STOMP 방식과 웹소켓(STOMP를 사용하지 않는 형태) 방식으로 각각 구현을 해봤었는데, 이번 글에서는 STOMP와 STOMP를 사용하지 않은 Raw한 웹소켓 방식의 차이점에 대해 정리하는 글을 써보려고 한다.

 

 인터넷에 STOMP 방식의 채팅 서버에 대해서는 정리가 많이 되있는거 같은데, 웹소켓만을 사용하는 형태에 대해서는 글이 별로 없는거 같아 이렇게 따로 정리해본다.

 

 

Web Socket이란?

웹에서 사용하기위한 소켓, 웹소켓

 

 게임서버나 하드웨어 장비통신에 주로 사용되는 Raw 레벨 소켓 통신의 경우 클라이언트에서 특정 Port를 열어서 서버에 연결 요청(Connect)을 보내고 이를 서버에서 수락(Accept)하는 일련의 Handshake과정이 필요하다. 다만, 웹 브라우저는 보안 이슈로 인해 자신의 임의의 Port를 열 수가 없는 구조이다.

 

 하지만 우리는 업무용 메신저 툴들의 웹버전이나 디스코드 웹버전같이 실시간 통신이 되는 웹서비스들을 많이 접해왔다. 이런 서비스들은 소켓 통신 없이 어떻게 실시간 서비스들을 구현했을까? 그 해답은 웹소켓에 있다. 웹에서 사용가능한 실시간 통신인 웹소켓 프로토콜을 사용했기때문에 웹 브라우저에서도 실시간 서비스를 구현할 수 있었다.

 

 

 

 

 웹소켓은 어떻게 동작하는가?

 

GET /ws/chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13

 

 

 일반적으로 웹소켓은 위와같은 형태의 HTTP요청을 전송하여 Connection 요청을 한다. 주의깊게 봐야할 부분은 Upgrade와 관련된 부분이다. 이 요청의 의미는 명확하다.

  • “이 TCP 연결을 HTTP로 쓰지 말고”
  • “WebSocket 프로토콜로 전환하자”

 

 

 

 

 

 여기서 헷갈릴 수도 있는게, 웹소켓은 HTTP가 아니다. 완전히 별도의 프로토콜이다. 그럼 소켓 통신이냐? 하면 좀 다르다. 네트워크 계층에서 정의하면, Raw 레벨의 tcp 소켓 통신은 전송 계층(transport)에 속한 프로토콜이며, HTTP와 웹소켓은 상위 계층의 Application 계층에 속한 별도의 다른 프로토콜들이다.

 

 

 사실 HTTP나 웹소켓이나 tcp위에서 동작하니까 웹소켓이나 HTTP나 똑같은거 아니냐 할 수도 있는데, 둘은 확실히 다르다. 그렇기 때문에 HTTP 요청을 통해 웹소켓 프로토콜로의 전환 요청을 통해 웹소켓 Handshake 과정을 거치는 것이다.

 

 

STOMP란?

 STOMP(Simple Text Oriented Messaging Protocol)메시지 브로커 스타일의 통신을 표준화한 텍스트 기반 프로토콜이며, 웹소켓 위에서 동작하여 웹소켓 통신을 보다 쉽게 사용할 수 있게 만들어주는 프로토콜이다.

 

 한 문장으로 요약하면 다음과 같다.

 

STOMP는 “소켓 위에서 메시지를 어디로 보내고(destination), 누가 구독(subscribe) 하는지를 표준화한 프로토콜이다.”

 

 

 

Raw 웹소켓 통신과의 차이점

 

 

WebSocket은 연결만 제공할 뿐, 다음은 아무것도 정의하지 않는다.

  • 메시지 형식
  • 채널 개념
  • 브로드캐스트 / 구독
  • 에러 처리 방식

즉, Raw WebSocket은 이렇게 묻는다.

“연결은 열어줄게.
그런데 메시지를 어디로, 누가, 어떻게 받을지는 네가 정해.”

 

 

 

 

 STOMP는 이 공백을 채우기 위해 등장했다. STOMP에 대해 알아보다보면 가장 많이 보이는 그림이다. STOMP의 동작방식에 대해 설명해주고있다. 유저는 특정 경로로 구독(subscribe)을 요청하고, 원하는 topic으로 메시지를 전송하면, STOMP내부 Broker를 통해 해당 topic을 구독한 Session들에게 메시지를 전송(Broadcast)한다.

 

 

 

 설명만으로는 이해가 안될수도 있으나 redis의 pub/sub 통신 방식과 동일하다. 

 

 

클라이언트(세션)는 특정 topic을 구독(subscribe)하고, 서버는 해당 topic으로 뿌려야할 메시지가 생기면 topic을 구독한 클라이언트(세션)들에게 메시지를 발행(publish)한다. 

 

 

 

 STOMP를 사용하지 않고 웹소켓 통신을 직접 구현하게 되면, SessionManager등의 클래스를 통해 유저 세션관리가 필요해지며, 어떤 유저 세션에게 메시지를 전송할지(브로드캐스트할 대상들)에 대한 로직도 별도로 작성이 필요하다.

 

 

 

무엇을 써야하는가?

 

STOMP를 사용한 방식과 WebSocket만 사용한 방식에 대한 솔직한 후기

 

 

 우선 내 사이드 프로젝트(web-talk)를 진행하면서 초기에는 STOMP를 도입하여 개발하였고, 이후 STOMP를 제거하고 Raw한 웹소켓 만을 사용한 방식으로 교체하였다. 나는 이 과정에서 STOMP를 사용한 방식과 WebSocket만 사용하는 방식 모두에 대해 직접 헤딩을 해봤고, 프로그래밍적인 솔직한 비교를 해보고자 한다.

 

 

 

 

publish/subscribe 개념을 통해 메시지를 간접적으로 다루는 STOMP

 

 

 우선 STOMP 방식은 절대적인 작성해야할 코드의 양이 적다. STOMP 내부의 메시지 Broker에서 유저의 연결을 알아서 관리하며, 메시지 브로드캐스트도 유저 세션의 구독 경로(Destination)에 따라서 연결된 세션이 있을 경우 알아서 전송한다. 따라서 개발자 입장에서는 메시지를 전송받았을 때의 비즈니스 로직에만 신경 써주면 된다.

 

 

 

 

세션 관리와 메시지 전송을 직접 다루는 (STOMP를 쓰지않는) Web Socket

 

 

 WebSocket만 사용하는 사용하는 방식은 SessionManager 등을 통해 연결된 세션을 직접 관리해줘야한다. 또한 채팅 서비스를 구현하고자 하는 경우 어떤 유저(어떤 방의 유저들에게 Broadcast)들을 타겟으로 메시지를 전송할 지를 직접 관리해줘야한다. 내 프로젝트(web-talk)의 경우 이러한 로직처리를 위해 별도의 RoomManager 클래스를 정의하여 관리해주었다.

 

 

 

 

 

왜 STOMP를 채택하지 않았나?

 

 사실 프로젝트 초기에는 대부분의 블로그에서 채팅 시스템 구현을 위해 STOMP를 사용할때의 장점을 강조하여 나도 이에 동의하였고, STOMP를 사용한 채팅서버를 만들었었다. 하지만 결론적으로 내 프로젝트는 STOMP를 사용하지 않았다. 두가지 이유가 있는데, 정리하면 아래와 같다.

 

 

1. 성능적인 이슈

 

 STOMP는 메시징 추상화를 통해 개발 생산성을 높여주지만, 그만큼 STOMP에대한 프레임 파싱, 브로커에서의 Destination 처리, 문자열 기반 라우팅이라는 추가 비용을 동반한다. 당연하지만 STOMP는 생산성을 올려주는 대신 그만큼 처리 비용이 더 커지게 되는 단점이 있다.

 

 

 또한 STOMP의 경우 세션에 메시지를 전송하는 함수를 직접 호출할 수 없는 구조이다. STOMP에서는 WebSocket send 호출이 프레임워크 내부로 완전히 감춰져 있기 때문에, Raw WebSocket에서 가능했던 송신 스레드 분리를 동일한 수준으로 구현할 수 없다.

 

 

private void send(WebSocketSession session, TextMessage message) {
  if (!session.isOpen()) return;
    
  try {
    // => 전송처리는 동기함수
    session.sendMessage(message);
  } catch (Exception e) {
    logger.warn("WebSocket send failed. sessionId={}", session.getId(), e);
  }
}

 

 WebSocketSession을 통해 메시지 전송하는 함수의 경우 동기함수(sendMessage)를 사용하는데, 이는 tcp 소켓 서버에서도 동일했지만, 효율적인 비즈니스 로직 처리 및 전송(네트워크 IO)을 위해 별도의 쓰레드에서 처리하도록하는 것이 좋다.

 

 실제로 내 프로젝트(web-talk)에서는 쓰레드의 효율적인 사용을 위해 별도의 SendQueue를 만들어서 해당 쓰레드에서만 메시지를 전송해주도록 처리해주었다.

 

 

 물론 내 프로젝트(web-talk)는 대규모 트래픽을 전제로 한 서비스가 아니기에 성능적인 이슈는 무시할 수도 있었다. 하지만, 웹소켓이라는 새로운 프로토콜에 대한 학습의 목적성도 있었다. STOMP는 메시징 추상화를 통해 개발 생산성을 크게 높여주지만, 그만큼 WebSocket 세션 관리, 송신 스레드, 네트워크 I/O 흐름이 프레임워크 내부로 감춰진다. 본 프로젝트에서는 이러한 부분을 직접 설계하고 통제해보고 싶었기 때문에 STOMP를 사용하지 않았다.

 

 

2. 웹 이외의 호환성

 

 

STOMP 자체는 범용 프로토콜처럼 보이지만, 실제 생태계를 보면 웹 환경에 강하게 최적화되어 있다.

  • JavaScript (stomp.js)
  • Spring WebSocket STOMP
  • SockJS 연계

 

즉, STOMP는 다음 전제를 깔고 설계된 경우가 많다.

 

“클라이언트는 브라우저다”

 

 

 하지만 우리가 알고있는 앱들(카카오톡, 디스코드, 이외의 업무용 메신저들...)은 어떤가? 웹버전도 지원하지만 기본적으로는 안드로이드 앱을 지원한다. STOMP는 웹한정으로는 좋은 프로토콜이지만, 그 외의 환경에서는 라이브러리 지원여부에 따라 확장이 가능할 수도, 아닐수도 있다.

 

 물론 내 프로젝트인 web-talk의 경우 내 시간의 한계로 현재 클라이언트는 웹버전만을 지원하지만, 나중에 시간을 내서 안드로이드 클라이언트 앱을 개발할 의향도 있다. 이런 관점에서 봤을때 내 프로젝트에서 STOMP를 제거하는게 맞다고 판단하였다.

 

 

 

 

 

정리하자면...

 

 

 `스프링 기반의 웹환경을 지원하는 채팅서버` 를 만든다고 하면 대부분의 블로그에서 STOMP 방식의 모델을 채택한다고 한다. 하지만 STOMP는 정답이 아니다. 현재 프로젝트의 상황에 따라 STOMP를 채택할지 말지를 결정하는 것이 좋다.