1. STOMP / SimpleBroker 연습해보기
Moyiza 서비스의 Club 도메인 구현이 대충 끝난 후, 채팅 서비스를 만들기로 했다.
구체적인 기획은 없었지만, 대충 Club, Oneday를 생성하면 채팅방이 생기고, 가입하는 사람들은 채팅방에 자동 가입되어 모임별 단톡방의 역할을 하는 것으로 정했다.
웹 소켓도 처음, 채팅 기능도 처음이었기에 프론트엔드의 요청대로 STOMP 프로토콜로 통신하기로 정했고, 메시지 broadcasting은 Spring에서 제공하여 간단하게 사용할 수 있는 SimpleBroker를 사용하기로 했다.
간단한 뷰와 함께 STOMP / SimpleBroker를 사용해볼 수 있는 레퍼런스가 있어 연습해보았다.
https://spring.io/guides/gs/messaging-stomp-websocket/
Getting Started | Using WebSocket to build an interactive web application
In Spring’s approach to working with STOMP messaging, STOMP messages can be routed to @Controller classes. For example, the GreetingController (from src/main/java/com/example/messagingstompwebsocket/GreetingController.java) is mapped to handle messages t
spring.io
우리 서비스의 요구사항은 여러 개의 단체방이 있어 해당 채팅방에 구독할 수 있어야 했기 때문에, 구독 endpoint별로 메시지가 구분되는 것을 실험하기 위해 레퍼런스에서 주어진 view를 조금 수정해 연습해보았다.


WebSocketMessageBrokerConfigurer를 구현해 WebSocket 연결 endpoint와, MessageBroker를 설정해주었다.
2. 채팅 서비스 구현

@MessageMapping은 해당 destination으로 들어오는 message를 잡아온다.

메시지를 받아서 다시 해당 채널에 발송할 때, 우리는 JWT 토큰을 Spring security filter에서 검증하는 방식을 사용하고 있었기 때문에 spring security를 타고오지 않는 STOMP 메시지의 유저 정보를 알 수 없었다. 이를 해결하기 위해 두 가지 방식을 생각했다.
1. JWT 토큰의 claim에 닉네임, 프로필URL을 세팅해두고, 모든 메시지의 헤더에 JWT 토큰을 담아서 요청하면, 이를 decode하여 메시지에 담아 전달
2. sessionId에 유저정보를 묶어서 저장
결국 나중에는 이런저런 이유로 2번과 같이 구현하게 되었지만, 이 때 당시에는 "session 정보를 관리하게 되면 JWT 토큰의 stateless 하다는 장점이 없어지는 것 아닌가?" 라고 생각하여 1번과 같이 구현하였다.
메시지 헤더에 JWT 토큰을 포함하여 이를 통해 id, nickname, profileURL을 얻고, 이를 response에 담아 전송하는 방식이다. 메시지 헤더와 sessionId, destination은 stompHeaderAccessor를 통해 stompHeader에 접근하여 얻을 수 있다.
2번의 구현도 시도는 해보았는데, webSocket handshakeInterceptor를 구현하여 handshake시에 JWT를 검증하고, 이를 sessionId에 저장하는 방식을 생각했으나, 프론트측에서 sockJS를 사용하기로 결정하여서 이는 불가능하게 되었다(sockJS 라이브러리 사용 시 보안상의 이슈로 websocket 연결 요청에 native header를 포함할 수 없다고 한다) https://github.com/sockjs/sockjs-client/issues/196
Sending Authorization Header on handshake? · Issue #196 · sockjs/sockjs-client
Is it possible to specify an Authorization header on the socket connection handshake?
github.com
3. 결과물 & 정리

간단한 채팅 서버를 만드는 것임에도 상당한 시간이 걸렸다.
1. 소켓 통신을 처음 해봐서 handshake가 어떻게 이루어지는지 몰랐고,
2. sessionId라는게 있다는 것을 몰라서 삽질도 많이 했으며,
3. 인증을 어떻게 할지 고민하느라 시간을 많이 보냈다.
4. 인증 방법을 결정한 후에도, STOMP 프로토콜에 대한 기본적 지식(메시지의 구성, COMMAND, 헤더에 뭐가있는지, destination과 sessionId의 존재와 어떻게 접근하는지)이 없어서 매 순간이 의문이었다.
5. 개발환경을 잘 만들자 : 구현 초반에는 프론트 코드를 clone해서 내 로컬에 띄울 생각을 안했다 (node 패키지 설치 등 안해본 것에 대한 두려움). 따라서 테스트를 해 보려면 내 code에 로그를 찍어두고, push / merge 하고, 빌드와 배포를 기다린 뒤에 프론트 개발 서버로 접속해서 요청을 날려봐야 했다. 이런 식으로 하니까 불필요한 commit도 많이 쌓일 뿐더러, 작업 속도도 느리고 경험적으로 굉장히 불쾌했다. 금방 구현할 줄 알고 개발환경에 신경을 쓰지 않았는데, 초기에 환경을 세팅하고 작업했으면 시간과 에너지를 많이 아낄 수 있었을 것 같다.
참고사항 : ChannelInterceptor에서 JWT를 검증해서 메시지 헤더에 달아줘도 될텐데, controller에서 한 이유

ChannelInterceptor의 presend 메소드는 STOMP 메시지가 channel에 발행되면 거쳐가는 메소드다. 모든 메시지를 intercept할 수 있으며, header를 확인하여 어떤 COMMAND인지 확인하고, 적절한 처리를 해줄 수 있다. 따라서 controller에서 JWT decode를 하는 것 보다는 presend에서 메시지를 가로채서 유저 정보를 검증하고, 메시지 헤더에 달아주면 되는 것 아닌가? 라고 생각했다. 하지만,,,, presend에서 stomp header를 set해주더라도, 막상 메시지를 처리하는 controller에 가면 헤더 정보가 감쪽같이 사라지는 것을 확인했다. 공식 문서 등 자료를 찾아봐도 이에 관한 내용은 없었고, stackoverflow에 올린 질문에서도 답변을 구하지 못했다. 아시는 분 있으면 댓글부탁드립니다...
+추가
https://stackoverflow.com/questions/65919245/get-principal-of-logged-in-user-in-websocket-controller
다음 글에는 redis를 사용하게 된 이유와, 안 읽은 사람 count 구현을 작성 하려고 한다.
'공부 > Project' 카테고리의 다른 글
Moyiza - 채팅 서버 구현 (3) : load test 해보기 (0) | 2023.06.23 |
---|---|
Moyiza - 채팅 서버 구현 (2) : redis 도입 / 읽은사람 카운트하기 (0) | 2023.06.22 |