본문 바로가기

2023년/멋쟁이사자처럼 팀프로젝트

[팀프로젝트] STOMP 활용한 프로젝트 채팅방 구현

팀프로젝트 웹 채팅 구현 결과물

프로젝트 간 구현해야될 내용이나 발생한 이슈들에 대해서의 자세다.

불편하니까 고쳐앉고 정신차리도록 하자

 

STOMP 활용한 실시간 채팅 구현


STOMP 가 뭐지?

  • Simple Text Oriented Messaging Protocol 의 약자.
  • WebSocket 위에서 동작하는 텍스트 기반 메세징 프로토콜이다.
  • 클라이언트와 서버가 전송할 메세지 유형, 형식, 내용들을 정의하는 매커니즘

STOMP는 HTTP 위에서 동작하는 Frame 기반 프로토콜

Frame 은 아래와 같은 형식을 가짐

COMMAND
header1:value1
header2:value2

Body^@

Frame 이란

출처 : https://hpbn.co/

  • Frame : 통신 상 가장 작은 단위
  • Message : 다수의 Frame 으로 이루어진 구조
  • Stream : 클라이언트 - 서버 맺어진 연결을 통해 주고받는 여러개의 Message

스프링에서 지원하는 STOMP 기능

  • Message 를 @Controller 의 메세지 핸들링 메서드로 라우팅 함
  • SimpleIn-Memory Broker 이용한 Subscribe 중인 다른 Client 에게 메세지 브로드캐스팅
  • 스프링은 RabbitMQ,ActiveMQ 같은 외부 Messaging System을 STOMP Broker로 사용할 수 있도록 지원

 

이러한 구조 덕분에 스프링 웹 어플리케이션은 HTTP 기반의 보안설정, 공통된 검증 을 적용할 수 있음

 

클라이언트는 특정 경로에 대해서 Subscribe 한다면, 서버는 원할때마다 클라이언트에게 메세지 전송이 가능

또한 클라이언트는 서버에 메세지 전달할 수있는데,

 

서버는 @MessageMapping 된 메서드를 통해 해당 메세지를 처리할 수 있다. 
서버는 Subscribe 한 클라이언트들에게 메세지를 브로드캐스팅할 수도 있다.

 

/topic ( one-to-many 의 pub-sub 관계 )

/queue ( one-to-one 의 message 교환 관계 )

 

* 현 프로젝트에서는 단순 pub-sub 관계로 표현할 예정.

 

서버는 MESSAGE 를 사용해 모든 Subscriber 들에게 메세지를 브로드캐스트 할 수 있다.

STOMP 브로커는 반드시 메세지 전달을 구독한 클라이언트들에게 전달해야함

 

웹 소켓 말고 왜 STOMP 사용하나요?

각 커넥션마다 WebSocketHandler를 구현하는 것보다 @Controller 적용된 객체를 이용해 조직적으로 관리를 할 수 있음( 메세지를 Controller 객체의 MessageMapping Annotation으로 라우팅시킬 수 있다. )

Stomp의 Destination(URI 경로)를 기반으로 Spring Security를 적용해 메세지를 보호할 수 있다.

STOMP 장점

WebSocket 만 이용할때 보다 더 풍부한 프로그래밍 모델을 제공할 수 있음

  • 1. 메세지 프로토콜을 만들고, 형식을 커스터마이징 필요없음
  • 2. 외부 브로커를 이용해 여러 서버를 관리할 수 있다.
  • 3. 커넥션마다 WebSocketHandler 에서 관리하는 것 보다 @Controller 를 이용해 조직적 관리 가능
  • 4. Spring Security 사용 가능

STOMP 사용

STOMP Message 헤더가 /pub 으로 시작하면 @MessageMapping 정보와 매핑 메서드를 호출
STOMP Message 헤더가 /sub 으로 시작하면 Message Broker 로 바로 라우팅됨.

 

서버

WebSocketConfig.java

@EnableWebSocketMessageBroker
@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/stomp/chat")
                //sockJs 클라이언트가 websocket handshake로 커넥션할 경로
                .setAllowedOrigins("http://*:*")
                //가능한 경로 설정 ( 전체 오픈 : 기호에따라 수정 )
                .withSockJS();
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/pub");
        // /pub 으로 시작하는 stomp 메세지의 경로는 @controller @MessageMaping 메서드로 라우팅
        config.enableSimpleBroker("/sub");
        // /sub 로 시작하는 stomp 메세지는 브로커로 라우팅함
    }
}

StompChatController.java

@Controller
@RequiredArgsConstructor
public class StompChatController {

    private final SimpMessagingTemplate template; //특정 Broker로 메세지를 전달

    //Client가 SEND할 수 있는 경로
    //stompConfig에서 설정한 applicationDestinationPrefixes와 @MessageMapping 경로가 병합됨
    //"/pub/chat/enter"
    @MessageMapping(value = "/chat/enter")
    public void enter(ChatMessageDTO message){
        message.setMessage(message.getWriter() + "님이 채팅방에 참여하였습니다.");
        template.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
    }

	//"/pub/chat/message" 로 날린 데이터에 대해서 /sub/chat/room + roomId 한 구독자들에게 해당 message를 전달
    @MessageMapping(value = "/chat/message")
    public void message(ChatMessageDTO message){
        template.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
    }
}

/pub 으로 날라온 내용들에 대해서 처리하는 컨트롤러

 

클라이언트

<script>
	// config 에서 설정한 endpoint로 websocket handshake 를 해준다.
	var sockJs = new SockJS("/stomp/chat", null, {transports: ["websocket", "xhr-streaming", "xhr-polling"]});
    // sockJs 를 stomp 에 넘겨준다.
    var stomp = Stomp.over(sockJs);
    // connect이 맺어지면 실행
    stomp.connect({}, function (){
        // subscribe(path, callback)으로 메세지를 받을 수 있음
        // 해당 채팅방에 대해서 구독을 함 + 콜백함수로 chat 에 대한 메소드를 실행 (대충 가져온 메세지를 html 띄움)
        stomp.subscribe("/sub/chat/room/" + roomId, function (chat) {
            var content = JSON.parse(chat.body);
            var writer = content.writer;
            var dateTime = new Date().toDateString();
            var str = '';

            if(writer === username){
                str = "<div class='col-6 offset-md-6'>";
                str += "<div class='alert alert-warning'>";
                str += "<b>" + writer + " : " + content.message + " : " + dateTime + "</b>";
                str += "</div></div>";
                $("#msgArea").append(str);
            }
            else{
                str = "<div class='col-6'>";
                str += "<div class='alert alert-success'>";
                str += "<b>" + writer + " : " + content.message + "</b>";
                str += "</div></div>";
                $("#msgArea").append(str);
            }
        });



        stomp.send('/pub/chat/enter', {}, JSON.stringify({roomId: roomId, writer: username}))
        // send(path, header, message)로 메세지를 보낼 수 있음
        // /pub 경로이므로 @MessageMapping 에 있는 endpoint로 이동
    });

    $("#button-send").on("click", function(e){
        var msg = document.getElementById("msg");
        stomp.send('/pub/chat/message', {}, JSON.stringify({roomId: roomId, message: msg.value, writer: username}));
        // 버튼 클릭시 /pub 경로이므로 @MessageMapping 에 있는 endpoint 이동
        // message endpoint는 해당 방을 구독한 사람들에게 메세지 전달함
    });
});
</script>

Message 처리 과정

1. 클라이언트는 config에서 정의한 endpoint 에 (/stomp/chat)  커넥션을 수행하고 STOMP 프레임들을 해당 커넥션으로 전송

 

2. 클라이언트는 /sub/chat/room + roomId 경로의 목적지 헤더를 가지며 SUBSCRIBE 프레임을 전송

2-1. 서버는 프레임을 수신하면 디코딩하여 Message 로 변환하고 메세지를 clientInboundChannel 전송

2-2. 해당 채널에서 메세지를 Message Broker 라우팅 후 브로커는 해당 클라이언트의 구독 정보를 저장

 

3. 이후 클라이언트는 /pub/chat/message 경로의 목적지 헤더를 가지고 메세지를 전송

3-1. /pub 은 메세지가 @MessageMapping 메서드를 가진 컨트롤러에서 핸들링 됨

 

4. 핸들링된 메서드에서 반환한 값은 스프링 Message 변환

4-1. Message 에 내용을 담은채로 목적지는 /sub/chat/room + roomId 로 설정

4-2. 변환된 메세지는 BrokerChannel 로 전송되며 브로커에 의해 처리됨

 

5. 마지막으로 브로커는 매칭된 모든 구독자를 탐색하고 clientOutboundChannel을 통해 각 구독자들에게 Message 프레임을 보냄

 

간략히.

스프링의 Message > STOMP 의 Frame 인코딩 > 연결된 Websocket 커넥션으로 Frame 전송

 

 

 

 

 

참고자료

https://velog.io/@koseungbin/WebSocket#stomp

 

WebSocket

이 글은 Spring WebSocket(https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.htmlWebSocket 프로토콜은 표준된 방법으로 서버-클라이언트 간

velog.io