===== 채팅서버 구현하기 시리즈 =====
- ✔️ [Web-Network] - 채팅 서버 설계를 위한 배경지식 정리 (HTTP, WebSocket, WebRTC)
- 👉🏻 [Spring/Spring Boot] - Spring WebSocket 공식문서 가이드 살펴보기
- ✔️ [Spring/Spring Boot] - Spring WebSocket STOMP 채팅 서버 구현하기 (with. JWT, Exception Handling)
- ✔️ [Spring/Spring Boot] - Spring Data mongoDB + mysql 사용하기 (with. queries)
spring websocket 공식문서를 따라해보면서 spring 에서는 websocket을 어떻게 사용해야하는지 알아보고자 합니다.
코드는 깃허브에 있습니다.
document : https://spring.io/guides/gs/messaging-stomp-websocket/
필요 조건
- 자바 17 이상
- Gradle 7.5+ 또는 Maven 3.5+
- spring boot 3.0+
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 인터페이스의 기본 메소드를 구현하여 메시지 브로커를 구성합니다.
- registry.enableSimpleBroker("/topic")
- 간단한 메모리 기반 메시지 브로커가 greeting message (sender 에게서 받은 message)를 "/topic" 접두사가 있는 대상의 클라이언트로 다시 전달할 수 있도록 enableSimpleBroker( )를 호출하여 시작합니다.
- 👏🏻 이말은 즉, 해당 websocket 을 사용할 때 새로운 메세지 브로커를 만들어 destinationPrefixes [파라미터 ex) "/topic"]로 들어오는 모든 주소로 메세지를 전송하겠다는 설정입니다.
- registry.setApplicationDestinationPrefixes("/app")
- 도착경로에 대한 prefix 를 설정하는 메서드 입니다. @MessageMapping 주석이 달린 메서드에 바인딩된 메시지의 "/app" 접두사를 지정합니다.
- 예제처럼 prefix를 "/app" 으로 설정했다면 "/topic/hello" 라는 토픽에 구독을 신청했을 때 실제 경로는 "/app/topic/hello" 가 됩니다.
- 이 접두사는 모든 메시지 매핑을 정의하는 데 사용됩니다. 예를 들어 /app/hello는 GreetingController.greeting() 메서드가 처리에 매핑되는 엔드포인트입니다.
- registry.enableSimpleBroker("/topic")
- 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 채팅, 별도의 메세지 브로커 사용 등을 적용해보고 기록을 남겨볼까 합니다 !
참고
- 공식문서 : https://spring.io/guides/gs/messaging-stomp-websocket/
- websocket 구현 : https://hyeooona825.tistory.com/89