Spring/Spring Boot

Spring WebSocket 공식문서 가이드 살펴보기

민돌v 2023. 6. 1. 13:48

===== 채팅서버 구현하기 시리즈  =====


 

spring websocket 공식문서를 따라해보면서 spring 에서는 websocket을 어떻게 사용해야하는지 알아보고자 합니다.
코드는 깃허브에 있습니다.

document : https://spring.io/guides/gs/messaging-stomp-websocket/

 

 

필요 조건

 


 

1) 종속성 추가

spring 에서 websocket을 사용하기 위해 먼저 종속성을 추가해줍니다. 저는 Gradle 을 사용했습니다.

//websocket
implementation 'org.webjars:webjars-locator-core'
implementation 'org.webjars:sockjs-client:1.0.2'
implementation 'org.webjars:stomp-websocket:2.3.3'

//front
implementation 'org.webjars:bootstrap:3.3.7'
implementation 'org.webjars:jquery:3.1.1-1'

 

gradle 전체 코드

  • spring 버전에 맞게 종속성 버전도 맞춰주었습니다.
plugins {
   id 'java'
   id 'org.springframework.boot' version '3.1.1-SNAPSHOT'
   id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
   compileOnly {
      extendsFrom annotationProcessor
   }
}

repositories {
   mavenCentral()
   maven { url 'https://repo.spring.io/milestone' }
   maven { url 'https://repo.spring.io/snapshot' }
}

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-web'
   implementation 'org.springframework.boot:spring-boot-starter-websocket'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
   annotationProcessor 'org.projectlombok:lombok'
   compileOnly 'org.projectlombok:lombok'

   // spring websocket
   implementation 'org.webjars:webjars-locator-core'
   implementation 'org.webjars:sockjs-client:1.5.1'
   implementation 'org.webjars:stomp-websocket:2.3.4'

   //front
   implementation 'org.webjars:bootstrap:5.2.3'
   implementation 'org.webjars:jquery:3.6.4'
}

tasks.named('test') {
   useJUnitPlatform()
}

 


 

2) Spring STOMP(Streaming Text Oriented Message Protocol) 환경 설정

이제 spring 프로젝트에서 WebSocket 및 STOMP 메세지를 활성하기 위한 Spring 구성을 해줄겁니다.

 

WebSocketConfig.class

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //해당 파라미터의 접두사가 붙은 목적지(구독자)에 메시지를 보낼
        registry.enableSimpleBroker("/topic");

        //전역적인 주소 접두사 지정 하기싫으면 ("/") 으로 두면 됨
        registry.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //"/gs-guide-websocket" 이라는 엔드포인트 추가 등록
        registry.addEndpoint("/gs-guide-websocket")
                .addInterceptors()
                //fallback - client 가 sockJS 로 개발되었을 때만 필요 (앱 통신 시 제거)
                .withSockJS();
    }
}
  • WebSocketConfig 클래스는 Spring 구성 클래스임을 나타내기위해 @Configuration을 주석을 달아 빈으로 등록합니다.
  • @EnableWebSocketMessageBroker 는 메세지 브로커가 지원하는 WebSocket 메시지 처리를 활성화 시킵니다.
  • configureMesssageBroker( ) 메소드는 WebSocketMessageBrokerConfigurer 인터페이스의 기본 메소드를 구현하여 메시지 브로커를 구성합니다.
    1. registry.enableSimpleBroker("/topic") 
      • 간단한 메모리 기반 메시지 브로커가 greeting message (sender 에게서 받은 message)를 "/topic" 접두사가 있는 대상의 클라이언트로 다시 전달할 수 있도록 enableSimpleBroker( )를 호출하여 시작합니다.
      • 👏🏻 이말은 즉, 해당 websocket 을 사용할 때 새로운 메세지 브로커를 만들어 destinationPrefixes [파라미터 ex) "/topic"]로 들어오는 모든 주소로 메세지를 전송하겠다는 설정입니다.
    2. registry.setApplicationDestinationPrefixes("/app")
      • 도착경로에 대한 prefix 를 설정하는 메서드 입니다.  @MessageMapping 주석이 달린 메서드에 바인딩된 메시지의 "/app" 접두사를 지정합니다.
      • 예제처럼 prefix를 "/app" 으로 설정했다면 "/topic/hello" 라는 토픽에 구독을 신청했을 때 실제 경로는 "/app/topic/hello" 가 됩니다.
      • 이 접두사는 모든 메시지 매핑을 정의하는 데 사용됩니다. 예를 들어 /app/hello는 GreetingController.greeting() 메서드가 처리에 매핑되는 엔드포인트입니다.

enableSimpleBroker() 메소드로 만들어지는, 메세지 브로커

  • registerStompEndPoints( ) 메서드는  "/gs-guid-websocket" 앤드포인트를 등록함으로써 WebSocket을 사용할 수 없는 경우 대체 전송을 사용할 수 있도록 SockJS 폴백옵션을 활성화 합니다.
    • SockJS 클라이언트는 "/gs-guide-websocket" 에 연결을 시도하고 사용 가능한 최상의 전송(websocket, xhr-streaming, xhr-polling 등) 을 사용합니다.
    • 📌 결국, 연결할 소켓 앤드포인트를 등록하는 메서드 입니다.

파라미터로 들어오는 경로들이 소켓 엔드포인트로 추가됨

 


 

3) Message Stomp Controller 클래스

이제 웹소켓 통신을위한 stomp 컨트롤러 클래스를 살펴보겠습니다.

package com.example.websocket.controller;

@Controller
public class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public GreetingResponse greeting(final HelloMessageRequest message) throws InterruptedException {
        // simulated delay
        // 클라이언트가 메시지를 보낸 후 서버가 메시지를 비동기식으로 처리하는 데 필요한 시간만큼 오래 걸릴 수 있음을 보여주기 위한 것
        Thread.sleep(1000);

        return new GreetingResponse("Hello, " + HtmlUtils.htmlEscape(message.name()) + " !!");
    }
}

위에서 설정한 웹소켓 앤드포인트로 들어오는 소켓 통신의 경로 중, @MessageMapping 어노테이션으로 특정 경로로 들어오는 메세지들을 @SendTo 에 명시된 경로로 뿌립니다.

즉, "/hello" 로 경로로 특정 사용자가 메세지를 보내면 → "/topic/greetings" 에 대해 구독하고 있는 사용자들에게 메세지 브로커가 메세지를 전달합니다. 


request / response dto 객체

  • 자바 17이라 그냥 record 로 했습니다.
public record GreetingResponse(String content){

}

public record HelloMessageRequest(String name) {

}

+

코드 참고 : https://hyeooona825.tistory.com/89

@Controller
public class ChatController {

    @MessageMapping("info")
    @SendToUser("/queue/info")
    public String info(String message, SimpMessageHeaderAccessor messageHeaderAccessor) {
        User talker = messageHeaderAccessor.getSessionAttributes().get(SESSION).get(USER_SESSION_KEY); 
        return message;
    }

    @MessageMapping("chat")
    @SendTo("/topic/message")
    public String chat(String message, SimpMessageHeaderAccessor messageHeaderAccessor) {
        User talker = messageHeaderAccessor.getSessionAttributes().get(SESSION).get(USER_SESSION_KEY); 
        if(talker == null) throw new UnAuthenticationException("로그인한 사용자만 채팅에 참여할 수 있습니다.");
        return message;
    }

    @MessageMapping("bye")
    @SendTo("/topic/bye")
    public User bye(String message) {
        User talker = messageHeaderAccessor.getSessionAttributes().get(SESSION).get(USER_SESSION_KEY);
        return talker;
    }
}

@SendToUser 라는 어노테이션이 존재하는데 1:N으로 통신하는 @SendTo 와 다르게 @SendToUser는 1:1로 통신한다고 합니다.

보통 sendTo 는 구독 경로가 /topic 으로 시작하며, sendToUser는 /queue로 시작하는데 보편적이라고 합니다.

 


 

4) Front 코드 찍먹

공식문서에는 친절하게도 html 코드와 js 코드가 나와있습니다. 코드를 전부 하나씩 보지는 않고 JS 코드만 조금 볼려고합니다.
(어차피 FE 코드 잘 모르기도 하구여..ㅎ)

 

1) connect

  • SockJS를 통해 소켓을 생성한 후 connect 메소드를 호출합니다.
  • SockJs() 를 생성할때 파라미터는 웹소켓을 설정했을때의 앤드포인트("/gs-guide-websocket")으로 설정해주어야 합니다.

2) subscribe

  • 해당 소켓이 구독할 경로를 설정합니다.
  • "/topic/greetings" 경로를 구독하고 있으므로 해당경로로 메세지가 발행되면 콜백함수를 통해 showGreeting() 함수를 실행하는 구조입니다.
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);
    });
  });
}

3) send

  • 각각의 파리미터는 (메세지를 보낼 경로, 헤더 정보[contentType 등과 같은], 데이터) 를 의미합니다.
function sendName() {
  stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}

4) disconnect

  • disconnect( ) 함수로 소켓연결을 해제할 수 있습니다. 
function disconnect() {
  if (stompClient !== null) {
    stompClient.disconnect();
  }
  setConnected(false);
  console.log("Disconnected");
}

 

 

 

👏🏻 결과

 

 

다음 포스팅에서는,
토이프로젝트에 직접 적용해보면서, 안드로이드 사용 환경에서 여러 사용자간 1:1 채팅, 별도의 메세지 브로커 사용 등을 적용해보고 기록을 남겨볼까 합니다 ! 

 

 

 


참고