StompJs와 SockJs

dev_shim·2022년 9월 26일
2
post-thumbnail

websocket에 대해 web자도 모르지만.. 팀원들과 간단한 웹 커뮤니티 플랫폼 개발을 하던중 채팅서비스를 도입해보고싶어서 도전해보게 되었다

SockJs


websocket과 비슷한 기능을 제공하는 브라우저 javascript 라이브러리라고 한다
브라우저와 웹 서버 사이에 짧은 지연시간 그리고 크로스 브라우징을 지원하기 때문에
크롬,사파리,파이어 폭스 그리고 websocket 프로토콜을 지원하지 않는 최신 브라우저에서도 해당 라이브러리의 api가 잘 작동하도록 지원하는 라이브러리이다.

STOMP


STOMP는 Simple Text Oriented Messaging Protocol의 약자이다. WebSocket 프로토콜은 Text 또는 Binary 두 가지 유형의 메시지 타입은 정의하지만 메시지의 내용에 대해서는 정의하지 않는다. 즉, WebSocket만 사용해서 구현하게 되면 해당 메시지가 어떤 요청인지, 어떤 포맷으로 오는지, 메시지 통신 과정을 어떻게 처리해야 하는지 정해져 있지 않아 일일이 구현해야 한다. 따라서 STOMP라는 프로토콜을 서브 프로토콜로 사용한다. STOMP는 클라이언트와 서버가 서로 통신하는 데 있어 메시지의 형식, 유형, 내용 등을 정의해주는 프로토콜이라고 할 수 있다. STOMP를 사용하게 되면 단순한 Binary, Text가 아닌 규격을 갖춘 메시지를 보낼 수 있다. 스프링은 spring-websocket 모듈을 통해서 STOMP를 제공하고 있다.

STOMP의 형식은 HTTP와 닮았다.

COMMAND
header1:value1
header2:value2

Body^@

클라이언트는 메시지를 전송하기 위해 COMMAND로 SEND 또는 SUBSCRIBE 명령을 사용하며, header와 value로 메시지의 수신 대상과 메시지에 대한 정보를 설명할 수 있다. 기존의 WebSocket만으로는 표현할 수 없는 형식이다. 이를 통해 STOMP 프로토콜은 Publisher(송신자)-Subscriber(수신자)를 지정하고, 메시지 브로커를 통해 특정 사용자에게만 메시지를 전송하는 기능 등을 가능하게 한다. 메시지 브로커는 Publisher로부터 전달받은 메시지를 Subscriber로 전달해주는 중간 역할을 수행한다고 생각하면 된다. 메시지 브로커의 동작 방식은 다음과 같다.

  1. 각각 A, B, C 라는 유저가 차례로 5번방에 입장한다.
  2. A가 5번방에서 채팅을 전송한다.
  3. 5번방 메시지 브로커(중재자)가 메세지를 받는다.
  4. 5번방 메시지 브로커가 5번방 구독자들(A, B, C)에게 메세지를 전송한다.
  5. 지금부터 5번방에 유저들이 참가하여 메시지를 주고받는 상황을 단계별로 살펴보자.

유저들은 채팅방에 입장함과 동시에 다음과 같이 5번 채팅방에 대해 구독(SUBSCRIBE)을 하게 되고, 메시지 브로커는 클라이언트의 SUBSCRIBE 정보를 자체적으로 메모리에 유지한다.

SUBSCRIBE
destination:/subscribe/chat/room/5

다음과 같이 어떤 유저가 메시지를 보내면, 메시지 브로커는 SUBSCRIBE 중인 다른 유저들에게 메시지를 전달한다.

SEND
content-type:application/json
destination:/publish/chat

{"chatRoomId":5,"type":"MESSAGE","writer":"clientB"}

Stomp 기반 구현

WebSocket 관련 설정 클래스를 추가해야 한다. 간단하게 요약하면 WebSocket 연결을 요청할 주소와 SUBSCRIBE, PUBLISH를 요청할 주소를 설정해주는 것이다.

@Configuration
@EnableWebSocketMessageBroker
public class ChatConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws/chat").setAllowedOriginPatterns("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        registry.enableSimpleBroker("/topic");

        registry.setApplicationDestinationPrefixes("/app");
    }
}

@EnableWebSocketMessageBroker

  • 메시지 브로커가 지원하는 ‘WebSocket 메시지 처리’를 활성화한다.

configureMessageBroker()

메모리 기반의 Simple Message Broker를 활성화한다. 메시지 브로커는 "/topic"으로 시작하는 주소의 Subscriber들에게 메시지를 전달하는 역할을 한다. 이때, 클라이언트가 서버로 메시지 보낼 때 붙여야 하는 prefix를 지정한다. 예제에서는 "/app"로 지정하였다.

registerStompEndpoints()

기존의 WebSocket 설정과 마찬가지로 HandShake와 통신을 담당할 EndPoint를 지정한다. 클라이언트에서 서버로 WebSocket 연결을 하고 싶을 때, "/ws-chat"으로 요청을 보내도록 하였다.


송신자와 수신자, 채팅방 번호 그리고 메시지 내용을 담고 있는 ChatMessage 클래스는 다음과 같다.

public class ChatMessage {

    public enum MessageType {
        ENTER, TALK , ALERT
    }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;
    
    @Column(name = "type")
    private MessageType type;
    
    @Column(name = "room_id")
    private Long roomId;
    
    @Column(name = "sender")
    private String sender;
    
    @Column(name = "message")
    private String message;

    @Column(name="nickname")
    private String nickname;

    @Column
    private String profileImg_url;
}

Controller는 다음과 같다.

@RestController
@Slf4j
@RequiredArgsConstructor
public class MessageController {

    private final SimpMessageSendingOperations sendingOperations;


    @MessageMapping("/chat/message")
    public void enter(ChatMessage message) {

        if (message.getType().equals(ChatMessage.MessageType.ENTER)) {
            message.setMessage(message.getSender() + "님이 입장하였습니다.");
        }
        sendingOperations.convertAndSend("/topic/chat/room/" + message.getRoomId(), message);

    }

@EnableWebSocketMessageBroker를 통해서 등록되는 Bean이다. Broker로 메시지를 전달한다.

@MessageMapping

Client가 SEND를 할 수 있는 경로이다. WebSocketConfig에서 등록한 applicationDestinationPrfixes와 @MessageMapping의 경로가 합쳐진다.(topic/chat/message)

클라이언트에서는 다음과 같이 연결하면 된다.

클라이언트가 해당 카테고리에 (ex:채팅방) 에 참여하면 그 커뮤니티의 Id(ctId)를 경로로 subscribe(/chat/room/{ctId})라는 경로를 구독시켰다.

const connect = () => {
    let Sock = new SockJS("http://127.0.0.1:8080/ws/chat");
    stompClient = over(Sock);
    stompClient.connect({}, onConnected, onError);
  };

  const onConnected = () => {
    setUserData({ ...userData, connected: true });
    stompClient.subscribe("/topic/chat/room/" + ctId, onMessageReceived);
    userJoin();
  };

메세지 전송 예시는 다음과 같다

  const sendValue = (e) => {
    e.preventDefault();
    if (stompClient) {
      var chatMessage = {
        sender: user.nickname,
        message: userData.message,
        status: "MESSAGE",
        type: "TALK",
        roomId: ctId,
        profileImg_url: user.profile_image_url + user.file_name,
      };
      stompClient.send("/topic/chat/message", {}, JSON.stringify(chatMessage));
      setUserData({ ...userData, message: "" });
    } else {
      registerUser();
    }
  };

4번 채티방에 메세지를 보내는 예시

클라이언트가 해당 커뮤니티(ex:채팅방) 에 참여하면 그 커뮤니티의 Id를 경로로 subscribe(/chat/room/{Id})라는 경로를 구독시켰다.

그리고 그 커뮤니티를 참여함과 동시에 userJoin()이라는 method를 실행시켜
message type 을 "ENTER" 로 서버로 보내 서버에서 "{사용자}님이 입장하셧습니다"라는
메세지를 /chat/room/4 경로로 메세지를 보내게 입력했다.



이왕 해본김에 mui도 사용해 봤다.
MUI link
https://mui-treasury.com/components/chat-msg/

결론

메모리 기반의 STOMP 메시지 브로커 대신 Kafka, RabbitMQ, ActiveMQ 등의 메시징 시스템을 이용할 수도 있으니 추가로 학습해서 사용해보고싶다.

후기

첫 프로젝트를 진행하면서 카카오톡 로그인을 구현한 이후로 해보고싶었던게 채팅기능이였다.
다른것과 마찬가지로 웹소켓도 처음이였기에 개념부터 잡고 혼자 React 컴포넌트를 만들어서 서버랑 연결이 될때까지 계속 메세지를 보내며 테스트를 했던게 기억에 남는다.

기능들을 구현하는데 집중하다보면 api를 사용할때도 그렇고 websocket을 사용할때도 그렇고 하나하나씩 원리를 이해해가며 구현해가는게 느리지만 좋은길인걸 느꼇다.

프로젝트가 끝난이후로 블로그,깃허브에 올리면서 다시 공부가 되는게 느껴진다.
트러블 슈팅이나 주요 기능 코드들을 프로젝트 진행과정에서부터 기록하는 습관을 들이려고 노력중이다.

Reference
https://tecoble.techcourse.co.kr/post/2021-09-05-web-socket-practice/

profile
개발 연습장

0개의 댓글