⚙️ backend/🌱 spring

[Spring] WebSocket을 사용하여 대화형 웹 애플리케이션 구축하기

dev!n 2023. 2. 11. 18:51

 

웹소켓은 TCP 위에 있는 얇고 가벼운 계층입니다. 따라서 메시지를 포함하기 위해 "하위 프로토콜"을 사용하는 데 적합합니다. 이 포스트에서는 Spring과 함께 STOMP 메시징을 사용하여 대화형 웹 애플리케이션을 만듭니다. STOMP는 하위 수준 WebSocket 위에서 작동하는 하위 프로토콜입니다.

 

spring.io의 포스트를 참고하여 작성하였습니다.

 

Spring | Home

Cloud Your code, any cloud—we’ve got you covered. Connect and scale your services, whatever your platform.

spring.io

 

만들 것

사용자 이름이 포함된 메시지를 수락하는 서버를 구축합니다. 이에 대한 응답으로 서버는 클라이언트가 가입한 대기열에 인사말을 푸시합니다.

애플리케이션 결과 미리 보기

요구 환경

Java 17 or later
Gradle 7.5+ or Maven 3.5+

빌드 환경

$ ./gradlew -v
------------------------------------------------------------
Gradle 7.6
------------------------------------------------------------

Build time:   2022-11-25 13:35:10 UTC
Revision:     daece9dbc5b79370cc8e4fd6fe4b2cd400e150a8

Kotlin:       1.7.10
Groovy:       3.0.13
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          17.0.6 (Eclipse Adoptium 17.0.6+10)
OS:           Mac OS X 13.0 aarch64

Gradle Dependency

build.gradle.kts에 다음을 추가

implementation("org.springframework.boot:spring-boot-starter-websocket")
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")

STOMP 메시지 서비스 필수 구성 요소

리소스 표현 클래스 생성

이 서비스는 본문이 JSON 객체인 STOMP 메시지에 이름이 포함된 메시지를 받습니다.

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class HelloMessage {

    private String name;
}
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Greeting {

    private String content;
}

메시지 처리 컨트롤러 생성

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); // simulated delay 1s.
        return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
    }
}

STOMP 메시징을 위한 스프링 설정

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 간단한 메모리 기반 메시지 브로커가 /topic 접두사가 붙은 대상에서 인사말 메시지를 클라이언트로 다시 전달할 수 있도록 설정
        config.enableSimpleBroker("/topic");
        // @MessageMapping이 달린 메서드에 바인딩된 메시지에 대해 /app 접두사 지정
        // 이 접두사는 모든 메시지 매핑을 정의하는데 사용된다
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // /gs-guide-websocket라는 엔드포인트를 등록하여 웹소켓을 사용할 수 없는 경우
        // 대체 전송을 할 수 있도록 SockJS 폴백 옵션을 활성화
        // 사용 가능한 최상의 전송(웹소켓, xhr-streaming, xhr-polling 등)
        registry.addEndpoint("/gs-guide-websocket").withSockJS();
    }
}

클라이언트

메시지를 주고 받을 자바스크립트 클라이언트

index.html과 main.css는 이 포스트의 관심사가 아니므로 공식 깃헙 레포에서 Copy&Paste 했습니다.

 

GitHub - spring-guides/gs-messaging-stomp-websocket: Using WebSocket to build an interactive web application :: Learn how to the

Using WebSocket to build an interactive web application :: Learn how to the send and receive messages between a browser and the server over a WebSocket - GitHub - spring-guides/gs-messaging-stomp-w...

github.com

index.html을 잠깐 보면, 웹소켓을 통해 STOMP를 통해 서버와 통신하는 데 사용되는 SockJS 및 STOMP 자바스크립트 라이브러리를 가져옵니다. 또한 클라이언트 애플리케이션의 로직이 포함된 app.js도 가져옵니다.

 

app.js

var stompClient = null;

function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    } else {
        $("#conversation").hide();
    }
    $("#greetings").html("");
}

/**
 * SockJS와 stomp.js를 사용하여 SockJS 서버가 연결을 기다리는 /gs-guide-websocket에 대한 연결을 엽니다.
 * 연결에 성공하면 클라이언트는 서버가 인사말 메시지를 게시하는 /topic/greetings 대상에 가입합니다.
 * 해당 대상에서 인사말이 수신되면 DOM에 단락 요소를 추가하여 인사말 메시지를 표시합니다.
 */
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");
}

/**
 * 사용자가 입력한 이름을 검색하고 STOMP 클라이언트를 사용하여 /app/hello 대상(GreetingController.greeting()이 수신하는 곳)으로 보냅니다.
 */
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();
    });
});

실행

application 클래스

이 애플리케이션을 실행하는데 사용합니다. Spring Boot가 애플리케이션 클래스를 자동으로 생성했을 것 입니다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

실행 가능한 JAR 빌드

// 실행
$ ./gradlew bootrun

// 빌드 후 JAR 실행
$ ./gradlew build
$ java -jar {패키지 이름}-{버전}.jar

실행 결과

서비스가 실행되면 브라우저에서 http://localhost:8080 에 접속하여 Connect 버튼을 클릭합니다.

연결이 열리면 이름을 입력하라는 메시지가 표시됩니다. 이름을 입력하고 보내기를 클릭합니다. 사용자의 이름이 STOMP를 통해 서버에 JSON 메시지로 전송됩니다. 1초의 시뮬레이션 지연 후 서버가 페이지에 표시될 "Hello" 인사말과 함께 메시지를 다시 보냅니다. 이때 다른 이름을 보낼 수도 있고 연결 끊기 버튼을 클릭하여 연결을 닫을 수도 있습니다.

더보기

command line ; ctrl + c 로 종료.

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.2)

~~~생략
2023-02-11T18:31:57.151+09:00  INFO 13732 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html]
~~~
2023-02-11T18:31:57.996+09:00  INFO 13732 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-02-11T18:31:57.997+09:00  INFO 13732 --- [           main] o.s.m.s.b.SimpleBrokerMessageHandler     : Starting...
2023-02-11T18:31:57.997+09:00  INFO 13732 --- [           main] o.s.m.s.b.SimpleBrokerMessageHandler     : BrokerAvailabilityEvent[available=true, SimpleBrokerMessageHandler [org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry@d082916]]
2023-02-11T18:31:57.997+09:00  INFO 13732 --- [           main] o.s.m.s.b.SimpleBrokerMessageHandler     : Started.
2023-02-11T18:31:58.010+09:00  INFO 13732 --- [           main] io.sookmyung.type.Application            : Started Application in 2.713 seconds (process running for 2.901)
2023-02-11T18:32:29.089+09:00  INFO 13732 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-02-11T18:32:29.090+09:00  INFO 13732 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-02-11T18:32:29.092+09:00  INFO 13732 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 2 ms
2023-02-11T18:32:43.601+09:00  INFO 13732 --- [nboundChannel-7] i.s.type.example.GreetingController      : HelloMessage(name=Jennie)
2023-02-11T18:32:46.048+09:00  INFO 13732 --- [boundChannel-10] i.s.type.example.GreetingController      : HelloMessage(name=Jennie)
2023-02-11T18:32:52.323+09:00  INFO 13732 --- [boundChannel-13] i.s.type.example.GreetingController      : HelloMessage(name=Bennie)
2023-02-11T18:32:57.039+09:00  INFO 13732 --- [MessageBroker-4] o.s.w.s.c.WebSocketMessageBrokerStats    : WebSocketSession[1 current WS(1)-HttpStream(0)-HttpPoll(0), 1 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(1)-CONNECTED(1)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 15, active threads = 0, queued tasks = 0, completed tasks = 15], outboundChannel[pool size = 4, active threads = 0, queued tasks = 0, completed tasks = 4], sockJsScheduler[pool size = 8, active threads = 1, queued tasks = 2, completed tasks = 5]
<==========---> 80% EXECUTING [1m 47s]
> :bootRun