brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Mar 19. 2022

Spring Websocket & STOMP

오랫만에 작성하는 기술블로그 포스팅입니다. 이 글에서는 스프링 부트 기반의 웹소켓 및 STOMP에 대해서 설명합니다. 이 글을 읽기 위해서는 기본적인 HTTP 지식이 있어야 하며, 스프링 프레임워크 개발 경험이 있어야 합니다. 또한, RabbitMQ 에 대한 이해가 없다면 4장은 읽기 어렵습니다.



이 글은 스프링을 집중해서 설명하며, 웹소켓 대한 자세한 개념은 생략하였으니 따로 공부하시길 바랍니다. 업무 외 시간 주말에 작성하였으며, 회사 소스 코드는 포함하지 않습니다. 샘플코드는 아래 github 을 참고하세요

https://github.com/sieunkr/spring-websocket-repo




1. WebSocket(웹소켓) 개요


웹소켓 경험자는 1장을 넘어가고 2장을 바로 읽어보길 바란다.


1.1 클라이언트가 서버와 통신하는 방법

일반적인 클라이언트-서버 아키텍처의 HTTP 통신은 아래 그림과 같다. 우리에게 익숙한 모델이다.

웹의 동작 방식에 대해서는 아래 링크를 읽어보길 바란다.

https://developer.mozilla.org/ko/docs/Learn/Getting_started_with_the_web/How_the_Web_works

필자가 아주 오래전에 포털 메인 화면의 데이터를 실시간으로 변경하는 기능을 개발한 적이 있다. 실시간 검색어, 주식정보, 날씨정보 같은 기능이다.

실시간 검색어는 주기적으로 업데이트가 되어야 한다. 참고로 현재 네이버, 다음 등 주요 포털에서 실시간 검색어 영역이 사라진 상황이다. 어쨋든, 당시, 데이터를 어떤 방식으로 실시간으로 업데이트 해줬을까? 사실 조금 무식한 방법으로 서비스를 구축했었다. 클라이언트(웹브라우저)에서 서버에 Ajax 통신을 주기적으로 요청한 후, 응답 받은 데이터를 화면에 렌더링하는 방식으로 개발했었다. 즉, 30초 또는 1분에 한번씩 클라이언트에서 서버에 실시간 검색어 최신 데이터를 요청하며, 최신 데이터를 가져와서 화면에 업데이트를 해주는 방식이다. 1분 사이에 실시간 검색어가 똑같을 수 있다. 하지만, 클라이언트는 실시간 검색어 데이터가 똑같은지, 변경되었는지 알수 없다. 그래서, 조금 무식하지만 주기적으로 계속 호출해서 변경된 데이터가 있는지 확인할 수 밖에 없다.

폴링


위와 같은 방식을, 조금 고급진 용어로 "폴링" 이라고 표현한다. (더 개선된 방법으로 롱폴링으로 개발할 수 도 있다.)


암튼, 당시에는 위 방법이 최선이었다. 브라우저마다 지원하는 기술이 조금씩 다르지만, 당시에는 인터넷 익스플러로가 사용 비율이 가장 높았던 시절이었다. 아휴... 정말 IE 모든 버전에 호환되도록 화면을 개발하는 일은 정말 짜증나는 일이었다. 인터넷 익스플로어를 포함해서 크롬, 파이어폭스 등 모든 웹브라우저에서 동작하도록 하기 위해서는 위 방식이 최선이었다.


하지만, 주기적으로 호출하는 위와 같은 폴링 방식보다는, 서버에서 클라이언트로 변경이 필요한 데이터를 푸쉬(전송)해주면 더 효율적일 것이다.


이때 우리가 생각해볼 수 있는 기술이 바로, "Server-Sent Events"(이하 SSE)이다. SSE 역시 HTTP 프로토콜에 의해서 동작한다. 대충 그림을 그려보면 아래와 같다.

Server-Sent Event


SSE는 실시간검색어 뿐만 아니라, 주식 정보, 날씨 정보 등의 데이터를 업데이트할때 사용하면 좋을 것이다. 서버에서 클라이언트에 단방향으로 제공하는 데이터이다. 단점으로는, 모든 브라우저에서 지원하지 않는다. 하지만, polyfill 등을 사용해서 어떻게든 해결할 수 있을 것이다. (최근에는 필자가 직접 해보지 않아서 자세히는 모르겠다.) 실시간 검색어, 주식, 날씨 정보와 같은 데이터는 양방향 통신이 필요 없다. 서버에서 업데이트가 필요한 시점에 클라이언트에 단방향으로 데이터를 전송해주면 된다.



반면에, 웹사이트에 채팅 기능을 만든다고 가정해보자. 채팅은, 상대방의 메시지를 실시간으로 전달 받아야하며, 내가 작성한 메시지를 상대방에게 실시간으로 보내야한다. 즉, 채팅 기능은 채팅을 하는 모든 사용자가 실시간으로 양방향 통신을 해야 한다. 이 경우, 폴링, SSE 등의 기술이 적합하지는 않다. 물론, 억지로 구현은 가능하지만, 복잡하고 비효율적인 설계가 될 것이다. 


그럼, 양방향 통신을 위한 대안은 어떤 기술이 있을까? 이때, 생각해볼 수 있는 기술이 바로 Websocket(이하 웹소켓)이다. 채팅 서비스는 내가 작성한 메시지가 상대방에게 실시간으로 전송이 되며, 상대방이 작성한 메시지를 실시간으로 내 화면에 보여줘야 한다. 채팅을 하는 모든 사용자는, 중앙 서버를 통해서 실시간으로 양방향 통신을 해야 한다. 필자의 허접한 그림을 보고 이해해보자.


사람1 채팅 화면: 안녕~

사람2 채팅 화면: (사람1에게 받은 메시지) 안녕~

사람2 채팅 화면: 응! 반가워~

사람1 채팅 화면: (사람2에게 받은 메시지) 그래 반갑다.


웹소켓은 TCP 연결을 통해서, 양방향 통신 채널을 제공하는 기술이며, 간단한 채팅 서비스는 웹소켓으로 어렵지 않게 만들 수 있다. 텍스트 채팅 뿐만 아니라, 화상 회의 서비스도 구현할 수 있을 것이다. 두명 이상의 접속자가 화상 회의 서비스를 한다고 가정하면 아래와 같을 것이다.

하지만, 화상 회의 서비스는 일반적으로 웹소켓이 아닌, WebRTC 기술을 사용한다. WebRTC 에 대해서는 필자의 글을 참고하길 바란다.


https://brunch.co.kr/@springboot/639

https://brunch.co.kr/@springboot/640


1.2 WebSocket 테스트 도구

웹소켓 테스트 도구를 소개한다.


Postman

맥북에서 Postman 을 사용해서 웹소켓 테스트를 할 수 있다.


크롬 Simple WebSocket Client



크롬 익스텐션을 설치해야 한다. 가볍게 쓸만하다. 단, STOMP 테스트를 지원하지 않는다.


Apic - Complete API solution

STOMP 테스트를 위해서 설치하였다. 그닥 좋지는 않다.

wscat

서버 또는 터미널에서 사용하기 좋다.

https://github.com/websockets/wscat


테스트 도구에 대한 자세한 설명은 생략한다. 이 글의 샘플 코드를 테스트하기 위해서는 반드시 테스트 도구에 익숙해야 한다.


1.3 웹소켓 이란?

웹소켓은 서버와 클라이언트 사이에 소켓 커넥션을 유지하면서, 양방향 통신이 가능한 기술이다. 웹소켓에 대해서 간단히 알아보자.


웹소켓은 어떻게 동작하나요?

웹소켓은 HTTP 로 Handshake 를 초기 통신을 시작한 후, 웹소켓 프로토콜로 변환하여 데이터를 전송한다. 아래 그림을 보자.

https://www.raywenderlich.com/13209594-an-introduction-to-websockets

먼저 클라이언트는 서버에 HTTP 프로토콜로 핸드쉐이크 요청을 한다. 서버에서는 응답 코드를 101 로 응답해준다.


postman 이라는 도구에서 웹소켓 연결을 했을 때는 아래와 같이 101 으로 응답받는 것을 확인할 수 있다.


웹소켓을 위한 별도의 포트를 오픈해야 하나요?

그렇지 않다. HTTP 또는 HTTPS 통신을 위해 오픈한 포트를 사용한다. 웹소켓은 HTTP 포트 80, 와 HTTPS 443 위에서 동작되도록 설계가 되었다. 별도의 포트를 오픈할 필요가 없다. 위에서 설명했지만, 호환을 위해서 핸드쉐이크는 HTTP upgrade 헤더를 사용하여, HTTP 프로토콜에서 웹소켓 프로토콜로 변경한다.


ws 와 wss 차이점은?

일반적으로 우리는 보안을 위해서 HTTP 통신이 아닌 HTTPS 을 해야 한다. 웹소켓 통신 역시 ws 가 아닌 wss 로 통신해야 시큐어 통신이 가능하다. 이 글에서는 편의상 ws 통신으로 설명하였지만, 회사 업무에서는 반드시 wss 로 구축하길 바란다.


웹소켓 통신은 실시간 양방향 통신을 위해 사용한다!

웹소켓은 일반적인 HTTP 통신과 다르게, 양방향 데이터를 실시간으로 전송할 수 있다. 그러기 위해서, 클라이언트는 서버에 소켓 연결을 한 상태로 유지된다. 언제든지 서버에서 보낸 데이터를 받을 준비를 하고 있는다. HTTP 통신은 지속적으로 데이터를 요청하는 폴링의 방식을 사용해야 하지만, 웹소켓은 그럴 필요가 없다. 그래서, 더 낮은 부하를 사용하여 클라이언트 와 서버 간의 실시간 통신이 가능하다.


이 글에서는 스프링이 핵심 주제라서, 웹소켓 자체에 대한 기본적인 개념은 이정도로 설명하고 넘어가겠다. 반드시 따로 공부해서 정리하길 바란다.


2. Spring WebSocket


스프링부트에서 웹소켓 연동하는 방법에 대해서 설명한다.


2.1 의존성

웹소켓 의존성을 추가해준다.

TextWebSocketHandler 상속받은 WebSocketHandler 클래스를 정의한다.

WebSocketHandler 클래스에 대한 상세한 내용은 2.2 에서 설명하겠다.


일단, WebSocketConfigurer 인터페이스를 구현한 WebSocketConfiguration 클래스를 작성한다.


1. 웹소켓 서버를 사용하도록 정의한다.  @EnableWebSocket

2. 웹소켓 서버의 엔드포인트는 url:port/room 으로 정의하였다.

3. 클라이언트에서 웹소켓 서버에 요청 시 모든 요청을 수용한다. (CORS)

- setAllowerOrigins("*") 으로 설정하였다. 실제 서비스 서버에서는 서버 환경에 맞게 변경해야 한다.

4. WebSocketHandler 클래스를 웹소켓 핸들러로 정의한다.


2.2 WebSocketHandler

서버 - 클라이언트 소켓 통신에서 사용하는 메시지 스펙을 정의하였다.

웹소켓 핸들러 클래스는 아래와 같이 4개의 메서드를 오버라이드 해야 한다.

- afterConnectionEstablished : 웹소켓 연결 시

- handleTextMessage : 데이터 통신 시

- afterConnectionClosed : 웹소켓 연결 종료 시

- handleTransportError : 웹소켓 통신 에러 시


2.2.1 WebSocket 최초 연결 시

최초 웹소켓 서버에 연결하면, 웹소켓 서버에 연결된 다른 사용자에게 접속 여부를 전달해주는 로직을 구현해보자. 채팅방이라고 생각하자. 채팅방에 이미 들어와있는 사용자에게 신규 멤버가 들어왔다는 것을 알려주는 것이다. 해당 로직을 구현하기 위해서는 기존 접속 사용자의 웹소켓 세션을 전부 관리하고 있어야 한다. 세션아이디를 Key로, 세션을 Value로 저장하는 MAP 자료 구조를 정의한다.

웹소켓 최초 연결 시 MAP 자료구조에 세션을 저장해둔다. 그리고, 접속중인 모든 세션에 메시지를 보내주도록 하자.

참고로, 본인에게는 접속 정보를 굳이 알릴 필요가 없어서, 자신을 제외한 나머지 세션에 보내도록 한다.


웹소켓 테스트를 위해서 크롬 확장도구의 Simple Web Socket Client 라는 도구를 사용하겠다.

웹소켓 서버에 연결을 성공하면 아래와 같이 Status : Opened 로 변경된다.


두번째 사용자가 연결되면, 첫번째 사용자에게 접속 정보 메시지를 발송한다.

첫번째 사용자가 받은 두번쨰 사용자의 접속정보에 세션 아이디를 받게 된다. 추후에, 해당 세션 아이디를 타겟으로 상대방에게 메시지를 발송하는 기능을 구현할 것이다. 이때 세션 아이디는 사용자마다 고유한 값이어야 한다.


세션 아이디는 어떻게 생성될까? WebSocketSession 인터페이스 의 구현체를 찾아보면 된다.

웹소켓 연결 시 WebSocketSession 에 어떤 구현체가 주입되는지 확인해보도록 하자.

StandardWebSocketSession 구현체가 주입되는 것을 확인할 수 있다. 해당 구현체에서 id 를 어떻게 생성해주는지 소스코드를 확인해보자.

소스코드를 보면 UUID 값이 생성되는 것으로 확인된다.

상세한 내용은 생략한다. 어쨋든, 사용자는 유니크한 세션 ID 를 배정받게 될 것이다. 그리고, 위와 같은 방법은 세션 정보를 메모리에 저장하는 방식이다. 서버가 재부팅되면, 세션 정보는 전부 사라지게 될 것이다.


지금까지, 클라이언트가 서버에 웹소켓 연결을 하는 과정이었다. 이제 데이터 통신을 구현해보자.  


2.2.2 WebSocket 데이터 통신 시

데이터 통신 시 handleTextMessage 메서드를 구현하면 된다.

첫번째 사용자가 두번째 사용자에게 메시지를 전송하겠다. 이때 반드시 메시지를 받을 타겟 사용자의 세션 아이디를 지정해야 한다. 첫번째 사용자는, 두번째 사용자가 접속했을 때 접속 정보를 받았는데, 접속 정보에 세션 아이디가 포함되어 있었다.


메시지 전송을 테스트해보자.

메시지 스펙에 맞게 JSON 메시지를 보내보자. 타겟 사용자의 세션 아이디값을 제대로 지정한다면 메시지 전송은 잘 될것이다.


2.2.3 WebSocket 연결 종료 시

첫번째 사용자를 강제로 종료해보자. 크롬 브라우저를 닫으면 된다. 그럼, 두번째 사용자는, 첫번째 사용자가 접속을 종료하였다는 메시지를 받을 것이다. afterConnectionClosed 메서드를 오버라이드하자.

테스트를 해보자.

상세한 설명은 생략한다.


2.2.4 에러 시

생략한다.


2.3 웹소켓 서버가 다수인 경우

해당 샘플 코드는, 웹소켓 서버가 1대인 경우에만 정상적으로 동작한다. 웹소켓 서버가 2대 이상이라면, 메모리 기반으로 관리하는 세션 정보를 서로 알아야 한다. (즉, 다른 서버에서 생성한 세션 정보를 서로 공유해야 한다.) 다양한 방법이 있다. 3장부터 설명할 STOMP 를 사용하는 방법, Redis Pub/Sub 를 사용하는 방법, 세션이 어떤 서버에 저장하고 있는지 접속 정보를 별도의 저장소에 저장하는 방법 등 수많은 방법이 있다. 웹소켓 서버가 다수인 경우에는 어떻게 시스템을 구축하면 좋을까? 이 글에서는 STOMP 에 대해서 설명할 예정이다.



3. Spring WebSocket STOMP


2장에서 설명한 방법은, 세션을 서버에서 따로 관리하도록 MAP 자료구조를 정의하였다. 또한 메시지를 어떻게 처리할지 직접 구현하고 있다. STOMP는 Simple Text Oriented Messaging Protocol 의 약자로, 메시징 전송을 효율적으로 하기 위한 프로토콜로, pub/sub 기반으로 동작한다. 메시지를 송신, 수신에 대한 처리가 명확하게 정의할 수 있다. 또한 WebSocketHandler 를 직접 구현할 필요 없이,  @MessaingMapping 같은 어노테이션을 사용해서, 메시지 발행 시 엔드포인트를 별도로 분리해서 관리할 수 있다.


3.1 PUB/SUB(발행/구독)에 대한 이해

발행/구독 아키텍처의 이해를 돕기 위해서 아주 허접한 그림을 그렸다...


두명의 사용자 클라이언트가 있다. 사용자는 서버와 웹소켓 으로 연결되어 있는데, 구독하는 주소를 동일하게 no01 에 구독하도록 설정하였다.

발행자 메시지의 타겟을 no01로 설정해서 메시지를 보냈는데, 서버에서는 발행자의 메시지를 확인한 후 no01 채널을 구독하는 모든 사용자(클라이언트)에게 메시지를 보내게 된다.


구독url 이 다른 사용자는 어떻게 될까? 아래 그림과 같이, 다른 구독 url 로 구독중이라면 메시지를 받지 못하게 된다.

구독과 발행 역할을 동시에 수행할 수 있다.


구독 과 발행을 동시에 하는 대표적인 예시는 채팅 기능이다. 채팅 메시지는 단방향으로 받기만 하지 않는다. 서로 메시지를 주고 받아야 한다.



더 자세히 알아보자

자... 조금만 더 구체적으로 알아보자. 각 구독자는 각각의 큐를 갖는다. 쉽게 이해하면, 개인 우편함이라 생각해도 된다. 아파트에 가면 각각의 호수 번호가 적혀있는 우편함을 볼 수 있다. 우편함에는 주소가 적혀 있을 것이다.


필자가 어렸을때는 신문을 배달 받았었다. (어린 친구들은 잘 모르겠지만...) 신문을 구독하면 아침마다 집 문앞에 신문이 놓여져 있다. 사용자 1, 2 는 조선일보 신문을 받고, 사용자3 은 한겨례 신문을 받기로 했다면, 조선일보 신문이 no01 이라는 구독 채널이고, 한겨례 신문이 no02 라는 구독 채널이라고 생각해도 된다. 각자 구독중인 신문만 받아야 하고, 각자 우편함이 따로 존재할 것이다.

암튼, 발행자는 반드시 채널 id 를 지정해서 전달해야 한다. 그래야 해당 채널을 구독중인 사용자에게 메시지를 보낼 것이다. 물론, 채널 아이디에 해당하는 구독자가 전혀 없다면, 메시지 전송은 되지 않을 것이다.


필자의 허접한 설명이 이해가 잘 되는가? 잘 안된다면... 그냥 넘어가길 바란다.. 


3.2 Spring WebSocket STOMP 구현

일단 메시지 스펙을 변경해보자. receiver 필드를 삭제하고, 채널아이디 필드를 추가하였다.

STOMP 를 사용하기 위해서, 아래와 같이 @EnableWebSocketMessageBroker 어노테이션을 선언해준다.

웹소켓 서버의 엔드포인트는 /ws 로 정의하였다.


클라이언트 사용자는 구독 경로를 "/sub/channel/채널아이디" 의 형태로 구독하도록 해보자.

메시지를 발송할 때는 "/pub/hello" 로 메시지를 보내며, 메시지에 채널아이디를 포함해야 한다. 위 코드만 작성하면, 스프링 프레임워크는 자동으로 STOMP 통신이 가능한, 웹소켓 서버를 실행시켜준다.


크롬 확장 도구에서 Apic 라는 도구를 사용해보자. 해당 도구에서는 STOMP 를 테스트할 수 있다.

먼저 구독자에 대한 웹소켓 연결을 해보자. 이때는 반드시 Subscription URL 을 지정해야 한다. 필자는 /sub/channel/eddy 라는 구독 URL 을 지정하였다.  



이번에는 메시지를 발행하는 역할의 새로운 크롬 브라우저를 실행한다. 이때는 구독URL 을 지정할 필요는 없다.

구독 URL 을 지정할 필요는 없지만, 메시지 발행 시 Destination Queue 에 /pub/hello 를 정의해야 한다. 또한 메시지 에는 channelId 에 반드시 구독 채널을 정확히 보내야 한다.


Send 버튼을 클릭하면 어떻게 될까? 아직은, eddy 채널에 구독중인 클라이언트에 아무 반응이 없을 것이다. 메시지 발행 시 채널에 메시지를 보내는 로직이 없기 때문이다. 아래와 같이 메시지 발행 로직을 작성해보자.

아래와 같이 크롬 브라우저를 두개 실행시켜서 테스트 해보았다.

메시지를 정상적으로 수신하는 것을 확인한다. channel id 가 다르면 메시지를 받지 않는다. 즉, eddy 라는 채널 아이디에 구독중인 사용자만 메시지를 받게 된다.


3.3 외부 메시지 브로커 필요한 이유

위 환경에서 메시지 브로커, 메시지 큐는 어디에 존재할까? 스프링 부트 서버의 내부 메모리에 존재한다. 하지만 웹소켓 서버가 다수일 때는 위와 같은 시스템으로 운영할 수 없다.


서버가 다수인 경우에 대해서 생각해보자. 서버가 2대이다. 사용자는 3명인데, 3명 모두 같은 채널을 구독하고 있다. 우리가 원하는 로직은 같은 채널에 구독중이라면, 발행 메시지는 사용자 1,2,3 모두에게 전송되어야 한다. 아래 그림과 같은 환경에서, 발행자가 메시지를 보내면 어떻게 될까?

발행자가 보낸 서버1에 구독중인 사용자 1, 2 만 메시지를 전달받게 된다. 사용자3은 메시지를 받지 못하게 된다.


위 경우를 개선하기 위해서는 어떤 방법이 있을까?


방법1

사용자가 어떤 서버에 접속하고 있는지 기억해두는 것이다. no01 이라는 채널을 구독하고 있는 사용자는 어떤 서버에 접속중인지 전부 저정해두면 된다. no01에 구독중인 사용자3은 서버2에 소켓 통신을 연결 중이라는 걸 어딘가에 저장하면 된다. 서버2에 no01 채널에 접속중인 사용자가 있다는 정보를 알수 있다면, 서버1에 발행 메시지를 보냈을 때, 서버1에서 서버2에도 접속중인 사용자를 알수 있다면, 서버2에 메시지를 보내도록 알려주면 된다.


방법2

외부 메시지 브로커에서 메시지 큐를 관리한다. 아래 그림과 같이 구축할 수 있다.

발행자는 채널 no01 에 구독중인 사용자에게 메시지를 보내고 싶다. 위 그림에서는 채널 no01에 구독중인 사용자 1,2,3 에게 메시지 전달을 할 수 있다. 사용자1, 2 는 서버1에 웹소켓을 연결 중이며, 사용자 3, 4 는 서버2에 웹소켓 서버에 연결 중이다. 발행자가 서버1 에 메시지를 보냈을때, 서버1에서는 메시지 브로커의 exchange에 메시지를 전달한다. exchange에 바인딩 되어있는 사용자1,2,3 큐에 메시지를 전달하게 되며, 서버2에 연결 중엔 사용자3 에도 메시지를 보낼수 있다. 외부 메시지 브로커를 사용하기 때문에 웹소켓 몇번 서버에 연결했는지 상관없이, 구독중인 채널의 메시지를 받을 수 있다.



필자의 글이 이해가 되지 않는다면, 스프링 레퍼런스 문서를 읽어보시길 바란다.

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket-stomp-message-flow

스프링 문서에서 나오는 그림 (외부 브로커 연동 전)

스프링 문서에서 나오는 그림 (외부 브로커 연동 후)


4장에서 외부 브로커 연동하자. 필자에게 가장 익숙한 RabbitMQ 를 사용하겠다.





인메모리 기반 시스템은 메시지 유실 가능성이 있다. 메시지 발행 시 서버가 down 되어서 메시지 전송을 못했다면 큐는 인메모리 기반으로 동작하기 때문에 메시지는 유실 될 것이다. 외부 메시지 브로커를 사용하고 있었다면, 서버가 재실행 시 외부 브로커에 저장중인 큐 에 대기 중인 메시지를 수신할 수 있다. 게다가, 인메모리 기반 시스템은 메시지 모니터링이 쉽지 않다. 여러가지 이슈로 인해서, 외부 브로커를 사용하는 방법이 좋을 것이다. 하지만, 무조건 외부 브로커 연동이 좋지는 않다. 외부 메시지 브로커를 사용하게 되면 인프라 비용이 증가하게 될것이다. 여러가지 상황을 고려해서, 시스템 아키텍처를 잘 구성해야할 것이다.





4장에서는, RabbitMQ 에 대해서 전혀 모른다면 이해하기 어려울 수 있습니다.

4. Spring WebSocket Stomp, RabbitMQ


3장에서 살펴본 인메모리 기반의 STOMP를, 외부 메시지 브로커를 연동하도록 변경해보자.


4.1 RabbitMQ 란?

RabbitMQ 에 대한 설명은 생략한다.


4.2 RabbitMQ 설치

로컬 개발환경에서는 간단하게 mac homebrew 로 설치하면 된다. 자세한 내용은 생략한다. RabbitMQ 서버가 실행되면 아래와 같이 관리도구에 접속할 수 있다. 기본 계정은 guest/guest 이다

웹소켓 서버를 실행하지 않았다면, no 커넥션 상태로 보일 것이다.


우리가 주목해야할 Exchange 는 amq.topic 이다.

물론, 아직 구독이 전혀 되어있지 않기 때문에 Queue 는 하나도 없을 것이다.


4.3 의존성 변경

3장에서 STOMP 를 사용할때는, 기본 spring-webSocket 의존성과 같았다. WebSocket 의존성 추가 시 STOMP 관련 의존성까지 같이 추가되기 때문이다. 하지만, RabbitMQ 를 연동하기 위해서는 별도의 의존성을 추가해줘야 한다.


4.4 Spring Boot WebSocket Stomp & RabbitMQ

아래와 같이, RabbitMQ 컨피그 설정이 필요하다.

웹소켓 서버에 대한 엔드포인트는 /ws 이다.


RabbitMQ 서버가 정상적으로 실행중이 아니라면, 애플리케이션 실행 시 에러가 발생할 것이다.

위와 같이 작성한 후 애플리케이션을 실행하면, STOMP 프로토콜에 구독할 수 있는 웹소켓 서버를 RabbitMQ를 연동해서 실행한다. 스프링은 정말 친절하지만, 내부 구현 방식을 꼼꼼히 살펴보는 것이 좋다. 개발자가 예상하지 못한 방식으로 동작할수도 있기 때문이다.


애플리케이션을 실행시켜 보자. RabbitMQ 에 커넥션이 연결된 것을 확인할 수 있다.

자, 이제 클라이언트에서 구독을 해보자. 구독 URL 은 반드시 /topic/채널아이디 의 형태로 해야 한다.

구독을 하게 되면, RabbitMQ 에서 Queue 가 생성이 된다. (3장에서 설명했던 내 우편함 으로 이해하면 된다.)

RabbitMQ 에서 중요한 점은, Queue는 Exchange 에 바인딩되고, 라우팅 키에 의해서 메시지를 발행한다는 것을 이해해야 한다. 위에 생성된 Queue 는 RabbitMQ 에서 기본으로 제공하는 Exchange 중에서 amq.topic 익스체인지에 바인딩 된다.

근데, 이때 좀 애매한게, 익스체인지는 토픽 타입으로 생성이 되며, 바인딩 키에는 #, * 등이 사용되지 않았다. 이게 무슨 의미냐면, 익스체인지 를 topic 로 생성하였지만, 실제로 direct 타입의 익스체인지와 유사하게 동작한다는 사실이다!!!!! (이해가 잘 안된다면, RabbitMQ 를 공부하고 돌아오길 바란다.)


메시지 발행을 구현해보자.

새로운 크롬을 실행해서 메시지를 발행해보자.

아래와 같이, 기존 크롬에 메시지가 정상적으로 수신하는 것을 확인할 수 있다.

만약, eddy 라는 채널에 다수의 클라이언트가 접속을 하였다면 어떻게 될까? 메시지 발행하면 구독중인 모든 클라이언트가 모두 동일한 메시지를 수신할 것이다. 그리고, 그렇게 되기 위해서는 메시지 브로커에서 각각 Queue 를 생성될 것이다. 아래 그림은 두명의 사용자가 eddy 라는 채널에 구독중인 상황에서, 두개의 큐가 생성되었다.

사실, RabbitMQ 는 메시지 큐잉 용도로 많이 사용하지만, 위와 같은 방식은 메시지 큐잉이라기 보다는 메시지 pub/sub 에 가깝다. 자세한 내용은 생략한다.


마지막으로 아래와 같이 이벤트 리스너를 구현할 수 있다.



4.5 Exchange 타입에 대한 의문

좀 이해가 되지 않는 상황이다. 다른 모듈에서도, 스프링에서 RabbitMQ 연동 시 익스체인지 타입을 Topic 로 정의하는 경우를 종종 볼 수 있었다. 아마도 Topic 타입이 더 유연하기 때문일 것이다. Topic 익스체인지는, 바인딩 키 설정에 따라서 direct 처럼 사용할수도 있고, fanout타입 처럼 사용할 수도 있다. 예를 들어서, 바인딩 키에 #, * 를 전혀 사용하지 않는다면, direct 처럼 사용한다. 바인딩키에 #만 설정했다면 fanout 처럼 사용하게 된다. 암튼 지금 내용은 RabbitMQ 를 잘 모른다면 이해하기 어려운 내용일수도 있고, 이 글의 주제와는 너무 벗어나기 때문에 이정도로 하고 넘어가겠다.


4.6 반드시 RabbitMQ 이어야 하는가?

ActiveMQ 등 다른 메시지 브로커 연동이 가능할 것이다. 필자가 해보진 않아서 생략하겠다.



5. RabbitMQ 대안


5.1 레디스 Pub/Sub

참고로, 레디스는 STOMP 프로토콜을 지원하지 않지만, Pub/Sub 기능을 사용하면 RabbitMQ 를 대체할 수 있다.

https://dgempiuc.medium.com/spring-websocket-and-redis-pub-sub-a02af0dabddb

https://brunch.co.kr/@springboot/374

자세한 내용은 생략하겠다.


5.2 기타

나중에 기회가 되면 공유하겠다.


마무리


스프링부트 기반의 웹소켓 서버에 대해서 설명하였다. 하고 싶은 얘기는 많지만, 시간 관계상 대충 마무리한다. 조금이라도 도움이 되었기를 바라며, 혹시라도 잘못된 내용이 있다면 댓글로 알려주길 바란다.








내용 추가 - Spring WebSocket ping/pong 에 대한 내용은 아래 글에서 확인하시면 됩니다.

https://brunch.co.kr/@springboot/801


브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari