ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • STOMP와 WebSocket으로 아주 간단한 메시징 시스템 만들기
    Spring 2021. 10. 19. 21:56

    Using WebSocket to build an interactive web application

    해당 프로젝트는 아래의 링크를 참고하여 구현했습니다.
    https://spring.io/guides/gs/messaging-stomp-websocket

    이 프로젝트는 브라우저-서버 간 메시지를 주고받는 응용프로그램을 작성하는 과정을 안내한다.

    WebSocket은 TCP위의 계층으로, 하위 프로토콜을 사용해서 메시지를 포함하기 적합하다.

    이 미니 프로젝트에서는 Spring과 함께 STOMP 메시징을 사용해서 대화영 웹 애플리케이션을 구현한다. STOMP는 하위 레벨 웹소켓 위에서 동작하는 프로토콜이다.


    Project Initialize

    image

    https://start.spring.io/ 에서 위와 같은 설정으로 프로젝트를 초기화해주었다.

    • Websocket 의존성을 추가해준다
    • Java11, Gradle 기반으로 SpringBoot 프로젝트를 구성했다

    Adding Dependencies

    build.gradle 파일 내의 dependencies 블록 안에

    implementation 'org.webjars:webjars-locator-core'
    implementation 'org.webjars:sockjs-client:1.0.2'
    implementation 'org.webjars:stomp-websocket:2.3.3'
    implementation 'org.webjars:bootstrap:3.3.7'
    implementation 'org.webjars:jquery:3.1.1-1'

    위와 같은 dependency 목록을 추가해준다.


    Create Resource Representation Class

    STOMP 메시지 서비스를 구축하기 위한 프로젝트 설정이 끝났다.

    서비스의 상호 작용을 구현하기 위해 다음과 같은 메시지 형식을 정의한다.

    {
      "name": "Ijin"
    }

    본문이 JSON 객체인 STOMP 메시지에 이름이 key-value 쌍으로 포함되어 있다.

    package com.example.stomp;
    
    public class HelloMessage {
        private String name;
    
        public HelloMessage() {
        }
    
        public HelloMessage(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }

    name 필드를 가진 메시지 클래스에 기본 생성자, getter, setter를 구현했다.


    메시지를 수신하고 이름을 받으면 서비스는 다음과 같은 JSON greeting 메시지를 만들어 클라이언트가 구독하는 대기열에 메시지를 게시한다.

    {
      "content": "Hello, Ijin!"
    }

    Message와 마찬가지로 Greeting 클래스를 정의한다.

    package com.example.stomp;
    
    public class Greeting {
        private String content;
    
        public Greeting() {
        }
    
        public Greeting(String content) {
            this.content = content;
        }
    
        public String getContent() {
            return content;
        }
    }

    Spring은 Jackson JSON 라이브러리를 이용해서 Greeting 타입의 인스턴스를 JSON으로 직렬화한다.

    Message 형식에 대한 정의를 완료하면 메시지를 보낼 컨트롤러를 정의한다.


    Message-Handling Controller

    Spring에서는 STOMP 메시지를 @Controller 어노테이션이 적용된 클래스로 라우팅한다.

    예시에서는 GreetingController를 정의해서 목적지가 /hello인 메시지를 처리하기 위해 다음과 같이 구현한다.

    package com.example.stomp;
    
    import org.springframework.messaging.handler.annotation.MessageMapping;
    import org.springframework.messaging.handler.annotation.SendTo;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.util.HtmlUtils;
    
    @Controller
    public class GreetingController {
    
        @MessageMapping("/hello")
        @SendTo("/topic/greetings")
        public Greeting greeting(HelloMessage message) throws Exception {
            Thread.sleep(1000);
            return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
        }
    }

    컨트롤러가 수행하는 역할

    • @MessageMapping 어노테이션은 목적지가 path와 일치하는 메시지를 수신했을 경우 해당 메서드를 호출한다.
    • 메시지의 Payload는 greeting() 메서드로 전달되는 HelloMessage 객체에 바인딩된다.
    • 내부적으로 Thread가 1초를 쉬었다가 전환되도록 해서 처리를 지연했다.
      • 서버가 메시지를 비동기식으로 처리하기 때문에 클라이언트가 메시지를 보낸 후 처리가 지연될 수 있음을 나타낸다.
      • 클라이언트는 응답을 기다리지 않고 필요한 작업을 계속할 수 있다.
    • greeting() 메서드는 Greeting 객체를 만들고 반환한다.
      • 반환 값은 @SendTo 어노테이션에 지정된 대로 해당 path의 모든 구독자들에게 브로드캐스트된다.
      • 입력 메시지의 name은 삭제된다(클라이언트의 브라우저 DOM에서 에코되어 렌더링이 다시 이뤄지기 때문)

    Configure Sprint for STOMP messaging

    서비스의 핵심 로직이 생성되었으니, WebSocket과 STOMP를 사용하기 위해 configure 해줘야 한다.

    다음과 같은 WebSocketConfig 클래스를 생성한다.

    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry config) {
            config.enableSimpleBroker("/topic");
            config.setApplicationDestinationPrefixes("/app");
        }
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/gs-guide-websocket").withSockJS();
        }
    }

    @Configuration@EnableWebSocketMessageBroker 어노테이션을 이용해 Spring Configuration 클래스임을 명시한다.

    @EnableWebSocketMessageBroker는 해당 클래스가 WebSocket의 메시지 브로커에 의해 웹소켓 메시지를 처리할 수 것임을 의미한다.

    configureMessageBroker() 메서드는 WebSocketMessageBrokerConfigurer에 정의된 메서드를 오버라이드해서 메시지 브로커를 구성한다. enableSimpleBroker()를 호출해서 간단한 메모리 기반의 메시지 브로커가 /topic prefix가 붙은 수신처(해당 prefix를 구독중인)의 클라이언트로 메시지를 전달할 수 있도록 한다. 또한, @MessageMapping 어노테이션이 붙은 클래스의 Path(/app)도 정의한다.
    예를 들어, /app/helloGreetingController.greeting() 메서드가 처리하도록 매핑된 엔드포인트이다.

    registerStompEndpoint() 메서드는 /gs-guide-websocket 엔드포인트를 등록해서 웹소켓을 사용할 수 없는 경우(ex. 브라우저 미지원) 웹소켓 대신 다른 전송방식을 사용할 수 있도록 SockJS 옵션을 활성화 한다.
    /gs-guide-websocket에 연결해서 사용 가능한 최적의 전송 방식(ex. 웹소켓, xhr-streaming, xhr-polling, etc)을 사용할 수 있도록 시도 한다.


    Create a Browser Client

    이제 브라우저를 이용해서 서버와 메시지를 송수신할 클라이언트를 만든다.

    src/main/resources/static/index.html에 html 파일을 생성한다.

    <!DOCTYPE html>
    <html>
    <head>
        <title>Hello WebSocket</title>
        <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
        <link href="/main.css" rel="stylesheet">
        <script src="/webjars/jquery/jquery.min.js"></script>
        <script src="/webjars/sockjs-client/sockjs.min.js"></script>
        <script src="/webjars/stomp-websocket/stomp.min.js"></script>
        <script src="/app.js"></script>
    </head>
    <body>
    <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
        enabled. Please enable
        Javascript and reload this page!</h2></noscript>
    <div id="main-content" class="container">
        <div class="row">
            <div class="col-md-6">
                <form class="form-inline">
                    <div class="form-group">
                        <label for="connect">WebSocket connection:</label>
                        <button id="connect" class="btn btn-default" type="submit">Connect</button>
                        <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                        </button>
                    </div>
                </form>
            </div>
            <div class="col-md-6">
                <form class="form-inline">
                    <div class="form-group">
                        <label for="name">What is your name?</label>
                        <input type="text" id="name" class="form-control" placeholder="Your name here...">
                    </div>
                    <button id="send" class="btn btn-default" type="submit">Send</button>
                </form>
            </div>
        </div>
        <div class="row">
            <div class="col-md-12">
                <table id="conversation" class="table table-striped">
                    <thead>
                    <tr>
                        <th>Greetings</th>
                    </tr>
                    </thead>
                    <tbody id="greetings">
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    </body>
    </html>

    웹소켓을 통해 STOMP로 통신하기 위해 html 파일의 헤더에서 SockJS와 STOMP 라이브러리를 import했다.

    image

    버튼으로 클라이언트 애플리케이션의 상호작용을 구현할 app.js 파일 또한 index.html과 같은 디렉토리 안에 만들어준다.

    var stompClient = null;
    
    function setConnected(connected) {
        $("#connect").prop("disabled", connected);
        $("#disconnect").prop("disabled", !connected);
        if (connected) {
            $("#conversation").show();
        }
        else {
            $("#conversation").hide();
        }
        $("#greetings").html("");
    }
    
    function connect() {
        var socket = new SockJS('/gs-guide-websocket');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            setConnected(true);
            console.log('Connected: ' + frame);
            stompClient.subscribe('/topic/greetings', function (greeting) {
                showGreeting(JSON.parse(greeting.body).content);
            });
        });
    }
    
    function disconnect() {
        if (stompClient !== null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log("Disconnected");
    }
    
    function sendName() {
        stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
    }
    
    function showGreeting(message) {
        $("#greetings").append("<tr><td>" + message + "</td></tr>");
    }
    
    $(function () {
        $("form").on('submit', function (e) {
            e.preventDefault();
        });
        $( "#connect" ).click(function() { connect(); });
        $( "#disconnect" ).click(function() { disconnect(); });
        $( "#send" ).click(function() { sendName(); });
    });

    이 js 파일의 핵심 부분은 connect()sendName() 메서드이다.

    connect() 메서드는 SockJS와 stomp.js를 사용해서 /gs-guide-websocket에 대한 커넥션을 생성한다.
    이 때 SockJS 서버는 연결을 대기하고, 성공 시 클라이언트는 서버가 greeting message를 publish 할 path인 /topic/greetings 를 subscribe한다.
    해당 topic에서 greeting 메시지를 수신하면 이를 표시하기 위해 DOM에 paragraph 요소를 추가한다.

    sendName() 메서드는 사용자가 입력한 이름을 STOMP 클라이언트를 이용해서 /app/hello로 전송한다. 이 메시지는 GreetingControllergreeting() 메서드로 전달된다.


    Make the Application Executable

    package com.example.messagingstompwebsocket;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class MessagingStompWebsocketApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(MessagingStompWebsocketApplication.class, args);
      }
    }

    스프링부트 프로젝트를 생성하면 자동으로 해당 Artifact Name + Application으로 된 class file이 생성된다.
    이 클래스는 @SpringBootApplicaion 어노테이션을 포함하고 있으며, 해당 어노테이션은 @Component, @AutoConfiguration, @ComponentScan의 어노테이션 사항들을 모두 포함한다.
    main 메서드는 SpringApplication.run() 메서드를 사용해서 애플리케이션 프로그램을 시작한다.


    Build an executable JAR

    Gradle 또는 Maven을 사용해서 애플리케이션을 실행할 수 있다. 필요한 모든 종속성/클래스/리소스가 포함된 단일 JAR 파일을 만들어서 실행할 수도 있다.
    실행 파일의 jar를 구축하면 개발 라이프사이클 전반, 다양한 환경에 걸쳐 서비스를 쉽게 제공할 수 있고 버전 지정과 배포도 간편해진다.

    이 프로젝트에서는 Gradle을 사용했기 때문에 ./gradlew bootRun을 사용해서 프로그램을 실행했다. 또는 ./gradlew build를 사용해서 jar 파일을 우선 생성한 뒤 실행할 수도 있다.

    java -jar build/libs/gs-messaging-stomp-websocket-0.1.0.jar

    빌드가 완료되면 로깅 메시지가 출력되고, 메시지 시스템을 테스트할 준비가 끝났다.
    😮


    Test the service

    http://localhost:8080을 통해 클라이언트에 접속할 수 있다(spring의 기본 view resolver의 path로 만들었으므로).

    Connection 버튼을 누르고 나면 소켓 연결이 성공되고 메시지를 송수신할 수 있다. 이름을 입력하고 Send 버튼을 누르면 이름이 JSON String으로 변환되고 STOMP를 통해 전송된다.
    서버는 이름에 "Hello"를 붙인 greeting 메시지를 보내고, 아래의 Greeting Table에 표시된다. 통신이 완료되면 Disconnect 버튼을 누르면 소켓 통신을 종료한다.

    image


    Summary

    STOMP 기반의 메시지 브로커를 이용한 아주 간단한 메시징 서비스를 구현해보았다.

    전체 프로젝트 코드는 아래의 Github 링크에서 확인할 수 있다 😄

    https://github.com/483759/message-system-practice/tree/main/stomp

Designed by Tistory.