[프로젝트] Spring STOMP 채팅 기능 [1]
배경
채팅 고도화 프로젝트를 진행함에 있어 과정을 기록하고자 본 포스팅을 하게 됐다. 이번 포스팅에서는 STOMP 프로토콜에 대해서 간단하게 알아보고 APIC 툴을 활용하여 채팅이 전송되는 것까지 확인할 것이다.
요구사항
어떤 서비스를 개발함에 있어서 요구사항을 잘 파악하는 것이 중요하다. 필자가 진행한 프로젝트에서 정의한 채팅의 요구사항은 다음과 같다.
- 실시간성 보장
- 유저가 많아질 때 대응할 수 있는 고가용성을 가져야 한다.
아래 후보군을 보자.
HTTP
채팅은 송신자가 발송하면 실시간으로 수신자가 이를 확인할 수 있어야 한다. 일반적인 HTTP 통신을 생각해보자. HTTP 통신의 경우 비연결성이라는 특성을 가진다. 요청 -> 응답을 받으면 연결이 바로 끊킨다. 이는 실시간 채팅에 적합하지 않음을 알 수 있다.
Polling
사진 출처 : https://warmth424.tistory.com/18
다음으로 Polling 방식을 생각해보자. Polling 방식은 클라이언트가 서버에게 일정한 주기로 요청을 해서 응답을 받아오는 방식이다. 만약 채팅이 없더라도 클라이언트는 서버로 일정 주기마다 요쳥을 보낼 것이다. 때문에 클라이언트의 수가 늘어나게 되면 서버에 굉장히 부담이 갈 것이다.
그렇다면 Long Polling은 어떨까? Long Polling은 클라이언트에서 요청을 걸어두고 서버에 특정 이벤트(채팅)가 발생할 때까지 기다렸다가 응답하는 방식이다. Polling 방식에 비해서 요청 횟수는 적지만 클라이언트의 수가 증가하면 서버에 부담이 많이 가는 것은 동일하다.
WebScoket
웹소켓은 단일 TCP 연결을 통해서 클라이언트와 서버 간의 전이중 양방향 통신을 제공하는 프로토콜이다.
출처 : https://innu3368.tistory.com/213
헤더에 보면 Upgrade에 websocket이 들어있는 것을 확인할 수 있다. 최초 연결은 HTTP로 진행하게 되고 Upgrade: websocket이 있으면 웹소켓 프로토콜로 전환되게 된다. 그래서 이후 프로토콜은 http/s가 아닌 ws/s가 된다.
웹소켓의 경우 TCP 연결을 끊지 않는다. 때문에 실시간성을 만족시킬 수 있고 다른 방식처럼 끊킴/연결이 발생하지 않기 때문에 지속적인 통신이 요구될 때 적합하다.
웹소켓은 우리 요구사항을 만족시킨다. 하지만 웹소켓을 사용하면 몇가지 단점이 있다.
- 웹소켓은 정해진 메시지 프레임이 없다. 때문에 서버와 클라이언트가 합의해서 메시지 프레임을 정의해서 사용해야 한다.
- 채팅방에 세션 관리를 직접 해줘야 한다.
물론 웹소켓만으로 잘 동작하는 채팅 서비스를 구현할 수 있다. 하지만 이번 프로젝트에서는 STOMP를 사용하여 좀 더 간편하게 채팅 기능을 구현해보고자 한다.
STOMP 특징
STOMP의 첫번째 특징은 pub/sub 구조를 가진다는 것이다.
여기서 말하는 pub/sub 구조란 무엇일까?
pub/sub 구조
옵저버 패턴을 생각하면 된다. 옵저버 패턴에서 Observer는 특정 Subject를 바라보고 있는다. 그리고 Subject에 특정 이벤트가 발생하면 이를 주시하던 모든 옵저버가 자신이 정의한 특정한 행위를 하게 된다.
이와 완전히 동일하다. 클라이언트들은 특정 채널을 subscribe한다. 그리고 만약 누군가 채널에 publish하면 이를 주시하던 모든 구독자들은 이를 받아볼 수 있게 된다.
두번째 특징으로는 메시지 프레임이 정의돼있다는 것이다.
메시지 프레임
사진 출처 : https://dev-gorany.tistory.com/235
위에 보면 알 수 있듯이 COMMAND, header, body로 나뉘어 있는 것을 볼 수 있다. 이와 같이 이미 메시지 프레임이 약속되어 있기 때문에 클라이언트, 서버간의 메시지를 정의할 필요가 없다.
이제 통신 흐름을 살펴보자.
통신 흐름
출처 : Spring 공식 문서
위 그림은 내장 브로커를 사용할 때의 메시지 흐름이다. 단일 WAS 환경에서는 문제가 없지만 서버를 다중화하게 되면 위 모델로는 제대로된 채팅 기능을 제공할 수 없다. 차차 개선하는 과정을 포스팅할 예정이니 우선 위 그림부터 이해해보도록 하자.
(여기서 /app = /pub, /topic = /sub이라고 생각하면 된다.)
총 3개의 channel이 있는 것을 볼 수 있다.
- clientInboundChannel : WebSocket 클라이언트로부터 받은 메시지를 전달하는 데 사용된다.
- clientOutboundChannel : WebSocket 클라이언트에 서버 메시지를 보내는 데 사용된다.
- brokerChannel : 서버 측 애플리케이션 코드 내에서 메시지 브로커로 메시지를 보내는 데 사용된다.
-
처음 요청이 들어오면 clientInboundChannel로 들어온다.
- 만약 서버 가공이 필요한 경우 /app 경로로 들어가서 서버에서 가공을 하고 brokerChannel를 통해서 /topic 경로로 브로커에게 전송한다. 가공이 필요하지 않다면 바로 브로커로 전송할 수도 있다.
- 마지막으로 clientOutboundChannel를 통해서 토픽을 구독하고 있는 구독자에게 메시지가 전송된다.
이제 기본적인 흐름은 다 살펴본 것 같다. 이제 코드를 보면서 위 과정을 하나씩 이해해보자.
STOMP 설정
- configureMessageBroker() 메서드에서는 메시지 브로커를 설정한다.
DestinationPrefixes를 /app으로 설정했는데 만약 STOMP 메시지의 헤더가 /app으로 시작하면 @MessageMapping로 라우팅되게 된다. 그리고 enableSimpleBorker() 설정을 통해 스프링 내장 브로커를 사용하도록 했고 /queue, /topic 경로로 들어오면 브로커에게 전송되도록 했다.
- registerStompEndpoints() 에서는 웹소켓 연결을 위해 핸드쉐이크할 HTTP URL Path이다.
/ws를 endpoint로 설정했다.
채팅방 생성
당연히 채팅을 하기 위해서는 둘 사이에 채팅방이 존재해야 한다. 채팅방 생성 로직을 살펴보자.
ChatRoomController:create
ChatRoomCreateRequest
ChatRoomService:createChatRoom
채팅을 할 상대의 Id를 통해 visitor 객체를 찾고 이 둘사이에 채팅방이 존재하는지 검증하고나서 채팅방을 생성한다. 로직이 워낙 단순해서 설명은 크게 필요없을 것 같다. 다음으로 채팅을 보내는 로직을 살펴보자.
채팅 전송
여기서 중요한 부분이 나온다. @MessageMapping과 sendOperations 객체이다.
설정 부분에서 살펴봤듯이 /app prefix를 가진 요청이 들어오면 @Controller의 @MessageMapping으로 라우팅된다고 했다. @MessageMapping에 추가로 /message 경로를 넣었기 때문에 /app/message로 요청이 들어오게 되면 해당 컨트롤러로 매핑된는 것이다.
여기서 sendingOperations.convertAndSend() 를 통해서 메시지가 /queue/chat-rooms/{roomId}를 구독하는 구독자에게 전송되게 된다. 그리고 메시지는 db에 저장되도록 했다.
MessageService:save
채팅 메시지를 저장하는 로직이다. 간단하게 채팅방에 송신자가 있는지 확인하고 메시지를 저장하게 된다.
이제 실제 테스트 결과를 보면서 마무리하겠다.
APIC 채팅 테스트
구독(subscribe)
우선 채팅방이 없으면 동작하지 않기 때문에 DB에 미리 charRoomId = 1인 채팅방을 하나 생성해두었다.
좌측 상단에 ws://127.0.0.1:8080/ws 로 서버와의 핸드쉐이크 경로를 지정했다. 그리고 Stomp 설정에서 구독할 경로 /queue/chat-rooms/1을 설정하고 서버와 연결했다.
publish
다음으로 송신자를 설정했다. 마찬가지 endpoint로 연결해주고 좌측 하단에 메시지를 publish할 경로를 설정하고 메시지를 Json form으로 작성했다.
이제 Send를 눌러서 실제로 채팅을 보내보겠다.
구독자에게 성공적으로 메시지가 전송된 것을 확인할 수 있다. 이상으로 STOMP 프로토콜을 활용한 채팅 서비스 구축하기 1편을 마무리하겠다.
다음편에서는 다중 WAS 환경에서 채팅이 잘 동작하도록 바꿔볼 것이다. redis, RabbitMQ, Kafka 순으로 브로커를 바꿔가며 테스트할 예정이다.