-
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
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/hello
는GreetingController.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했다.
버튼으로 클라이언트 애플리케이션의 상호작용을 구현할
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
로 전송한다. 이 메시지는GreetingController
의greeting()
메서드로 전달된다.
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 버튼을 누르면 소켓 통신을 종료한다.
Summary
STOMP 기반의 메시지 브로커를 이용한 아주 간단한 메시징 서비스를 구현해보았다.
전체 프로젝트 코드는 아래의 Github 링크에서 확인할 수 있다 😄
https://github.com/483759/message-system-practice/tree/main/stomp
'Spring' 카테고리의 다른 글
(kotlin)gradle spring boot application dockerize (0) 2023.09.28 Spring Dependency Injection의 종류 - Constructor, Setter, Field (401) 2021.10.16 Spring에서는 DI를 통해 IoC를 구현한다 (416) 2021.10.16 Spring AOP(Aspect-Oriented Programming)의 이해 (407) 2021.10.03 @(Annotation)을 이용한 Spring Container Configuration (449) 2021.10.02