Spring Boot 에서 채팅기능을 구현하는 방법은 여러 가지가 있다. 그중 가장 기본적인 방법은 'websocket' 을 이용하여, 직접 서버에 접속한 클라이언트들의 세션을 관리하면서, 메시지를 각 세션들로 전송해주는 것이다. 이때, 개발자는 전달할 메시지의 포맷을 직접 정의하고, 해당 메시지가 어떤 요청인지, 어떻게 처리해줘야 하는지 모두 구현해야 한다.
이번 포스팅에서 다룰 STOMP 는 위의 작업을 간소화하여, 보다 편리하게 메시지를 전송할 수 있도록 도와주는 프로토콜이다.
❓STOMP (Simple Text Oriented Message Protocol) 란
STOMP는 웹소켓 위에서 동작하는 텍스트 기반 메세징 프로토콜로써, 클라이언트와 서버가 전송할 메세지의 유형, 형식, 내용들을 정의하는 매커니즘이다. STOMP 의 특징은 다음과 같다.
- TCP 또는 웹소켓과 같은 신뢰할 수 있는 양방향 스트리밍 네트워크 프로토콜에서 사용할 수 있다.
- 기본적으로 pub/sub 구조로 되어있어, 메세지를 전송하고 받아 처리하는 부분이 확실히 정해져있다.
- 비동기적으로 메시지를 주고 받는다.
- http와 마찬가지로 frame을 사용해 전송하는 프로토콜이다.
- 헤더 값을 기반으로 통신 시 인증 처리를 구현할 수 있다.
📍 STOMP를 사용하는 이유
STOMP를 사용하는 이유는 메시징 처리가 간편하다는 이유도 있지만, 또 다른 이유도 있다.
일반적으로 채팅 시스템은, 한 명의 사용자가 여러 채팅방을 만들어 메시지를 주고 받을 수 있다. 그런데, WebSocket은 클라이언트와 서버 간에 하나의 연결(세션)만 관리하기 때문에, 채팅방을 여러 개 만들지 못한다는 문제가 있다. (사실, 아예 방법이 없는 것은 아니고, 채팅 메시지에 roomId와 같은 데이터를 별도로 넣고, 어떤 방에 어떤 메시지를 보낼지, 누구에게 보내야하는지를 직접 관리하면 채팅방을 구현할 수 있겠지만 몹시 번거롭다.)
이를 해결하기 위해 STOMP를 이용하여 pub/sub 구조로 여러 방을 만들고 STOMP Broker를 통해 특정 Topic을 구독중인 클라이언트들에게 메시지를 전달할 수 있다.
📄 STOMP Frame 구조
일반적인 STOMP Frame의 구조는 다음과 같다.
COMMAND
header1 : value1
header2 : value2
Body^@
클라이언트는 메세지를 전송하기 위해 SEND, SUBSCRIBE의 두 가지 COMMAND 를 사용할 수 있다.
SEND, SUBSCRIBE COMMAND 요청 Frame에는 메세지가 무엇이고, 누가 받아서 처리할지에 대한 Header 정보가 포함되어 있다. 이런 명령어들은 "Destination" 헤더를 요구하는데, 이것이 어디에 전송할지, 혹은 어디에서 메세지를 구독할 것 인지를 나타낸다.
다음은 SEND, SUBSCRIBE 두 가지 COMMAND에 대한 요청 Frame 예시이다.
SEND
destination: /pub/chat
content-type: application/json
{"chatRoomId": 10, "type": "MESSAGE", "writer": "clientB"} ^@
SUBSCRIBE
destination: /sub/chat/room/10
id: sub-1
^@
STOMP 서버는 모든 구독자에게 메세지를 Broadcasting하기 위해 MESSAGE 라는 COMMAND를 사용할 수 있다. 서버 메세지의 "subscription-id" 헤더는 클라이언트 구독의 "id"헤더와 일치해야 한다.
다음은 MESSAGE COMMAND에 대한 Frame 예시이다.
MESSAGE
destination: /sub/chat/room/10
message-id: d4c0d7f6-1
subscription: sub-1
{"chatRoomId": 10, "type": "MESSAGE", "writer": "clientB"} ^@
✅ 스프링이 지원하는 STOMP
STOMP를 서브 프로토콜로 사용하면, 앞서 말했듯 메시지의 형식이나 처리 방식들을 개발자가 정의할 필요도, 세션을 직접 관리할 필요도 없다. 또한, 스프링에서 WebSocketHandler를 직접 구현할 필요없이, @MessageMapping과 같은 어노테이션을 사용해서 메시지 발행 시 엔드포인트를 별도로 분리해서 관리할 수 있다. 스프링은 spring-websocket 모듈을 통해 STOMP를 제공한다.
Spring STOMP 구조
Spring의 STOMP 기능을 활성화하면 아래와 같은 구성으로 컴포넌트가 컨텍스트에 등록된다.
(/app = /pub, /topic = /sub)
용어 정리
- Message : headers와 payload를 포함하는 메세지의 표현
- MessageHandler : Message 처리에 대한 계약
- SimpleAnnotationMethod : @MessageMapping 등 Client의 SEND를 받아서 처리한다.
- SimpleBroker : Client의 정보를 메모리 상에 들고 있으며, Client로 메세지를 보낸다.
- channel
- clientInboundChannel : WebSocket Client로부터 들어오는 요청을 전달하며, WebSocketMessageBrokerConfigurer를 통해 intercept, taskExecutor를 설정할 수 있다.
- clientOutboundChannel : WebSocket Client로 Server의 메세지를 내보내며, WebSocketMessageBrokerConfigurer를 통해 intercept, taskExecutor를 설정할 수 있다.
- brokerChannel : Server 내부에서 사용하는 채널이며, 이를 통해 SimpleAnnotationMethod는 SimpleBroker의 존재를 직접 알지 못해도 메세지를 전달할 수 있다. (서버의 어플리케이션 코드 내에서 브로커에게 메세지를 전달)
Spring STOMP가 동작하는 과정은 다음과 같다.
1. 클라이언트가 보낸 메시지가 request channel에 들어간다.
2. 메시지는 목적지에 따라 SimpAnnotationMethodMessageHandler 또는 SimpleBrokerMessageHandler를 통해 처리된다. SimpAnnotationMethodMessageHandler로 처리될 때는 처리된 메시지가 brokerchannel을 통해 SimpleBrokerMessageHandler에게 전달된다.
3. SimpleBrokerMessageHandler는 메시지 타입에 맞게 메시지를 처리한다.
4. 클라이언트에게 메시지를 보내야 한다면 response channel을 통해 전달한다.
💡 외부 브로커를 사용하는 이유
Spring에서 STOMP를 사용하면 기본적으로 스프링 built in 브로커인 ‘Simple Broker’를 사용하게 된다. Simple Broker는 인메모리 브로커이기 때문에 두 명의 사용자가 각각 다른 서버에 접속한다면, 특별한 설정을 해주지 않는 한, 서버들의 세션을 공유할 수 없어 이 둘은 통신할 수 없다.
만약, 채팅 기능을 제공하는 서버 구성을 2대 이상으로 하였다면, 외부 메시지 브로커가 필요하다. 외부 메시지 브로커로는 Redis, AcitiveMQ, RabbitMQ, Kafka 등을 사용할 수 있다.
다음 포스팅에서는 Spring STOMP 와 외부 브로커를 이용해서 어떻게 채팅 기능을 구현하였는지 다뤄보겠다.
References
https://yoo-dev.tistory.com/51
https://velog.io/@hoyun7443/WebSocket%EC%9D%98-Stomp
https://hyuk0309.tistory.com/20
https://dev-gorany.tistory.com/235
'TIL' 카테고리의 다른 글
[TIL] 네트워크 기본 정리 (0) | 2024.08.26 |
---|---|
[TIL] 데이터베이스 정규화 (0) | 2024.08.12 |
[TIL] Interrupt 란? (3) | 2024.07.15 |
[TIL] SSL/TLS (0) | 2024.07.07 |
[TIL] CORS 문제와 해결법 (1) | 2024.07.01 |