STOMP 기반 채팅 구현
우선 build.gradle에 아래와 같은 의존성을 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.0'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
코드는 단순하게 구성하였다.
설정 파일인 WebSocketConfig를 만들고,
메세지 객체로 쓰일 Message를 만든다.
그리고 ChatController를 통해 라우팅을 해서 Message를 전달할 것이다.
WebSocketConfig
package com.example.stomp.config;
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 registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 테스트할 때만 setAllowedOrigins("*"); 허용
registry.addEndpoint("/ws").setAllowedOrigins("*");
}
}
WebSocketConfig를 만들 때, 아래의 두 가지 메서드를 오버라이딩해야 한다.
코드를 설명하자면,
@Configuration을 붙여서 스프링 컨테이너에 등록하고, 설정 파일임을 알릴 수 있다.
@EnableWebSocketMessageBroker를 붙이면 메세지 브로커를 사용해서 메세지 핸들링이 가능해진다.
configureMessageBroker는 메세지 브로커를 설정하는 메서드이다.
enableSimpleBroker는 "/sub"를 구독한 클라이언트에게 메세지를 발신함을 정의하고,
setApplicationDestinationPrefixes는 클라이언트가 메세지를 발행할 때, 브로커의 목적지에 접두사를 정의하는 것이다.
그리고 @MessageMapping 어노테이션이 붙은 컨트롤러가 호출되는 식이다.
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
registerStompEndpoints는 엔드포인트를 등록하는 메서드이다.
/ws가 붙은 엔드포인트로 통신이 이루어질 것이다.
현재는 편의상 setAllowedOrigins("*")로 해서 모든 요청을 받아들이도록 했지만,
실제 서비스에서는 특정 도메인에서만 허용되게 해야 할 것이다.
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 테스트할 때만 setAllowedOrigins("*"); 허용
registry.addEndpoint("/ws").setAllowedOrigins("*");
}
Message
package com.example.stomp.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Message {
private Long channelId;
private String sender;
private String message;
}
ChatController
package com.example.stomp.controller;
import com.example.stomp.model.Message;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Controller;
@Controller
@RequiredArgsConstructor
@Slf4j
public class ChatController {
private final SimpMessageSendingOperations operations;
// /pub/channel
@MessageMapping("/channel")
public void sendMessage(Message message) {
operations.convertAndSend
("/sub/channel/" + message.getChannelId(), message);
log.info("메세지 전송 성공");
}
}
위에서 설명했듯이,
setApplicationDestinationPrefixes는 @MessageMapping이 붙은 메서드를 호출한다.
즉 메세지를 발행하게 되면, /pub/channel 로 라우팅되고,
/sub/channel/channelId를 구독하는 클라이언트로 메세지 객체가 전달된다.
그리고 메세지가 잘 전송되었는지 로그도 찍어보자.
테스트
테스트는 apic이라는 툴을 사용할 것이다.
그동안 RestAPI를 만들 때, Postman을 주로 사용했지만,
Postman은 STOMP가 지원되지 않는다.
apic을 설치하고, 아래와 같이 설정해주자.
url은 ws://localhost8080/ws 이다.
ws는 http처럼 websocket 통신을 나타내는 의미이다.
보안성을 보장하는 https처럼 wss도 존재한다. 실제 서비스에서는 wss를 사용하자.
그리고 stomp를 선택해주고 오른쪽 상단에 connect를 누르자.
정상적으로 커넥션이 연결되면 Stomp connected라고 뜬다.
*참고로 발행하는 쪽과 구독하는 쪽을 둘다 테스트하기 위해 창을 2개 띄우자.
그리고 스프링 서버로 돌아와서 로그를 확인해보자.
2개의 WebSocket 세션이 생긴 것을 확인할 수 있다.
커넥션이 잘 되었는지에 대한 정보.
stomp 프로토콜로 2개의 통신이 연결되었다.
외부 메세지 브로커가 없으므로, stompBrokerRelay는 null이다.
인바운드 채널에 대한 정보.
아웃바운드 채널에 대한 정보.
자, 이제 메세지를 보내보자.
Publisher
/pub/channel로 메세지 브로커에게 메세지를 전달할 것이다.
메세지 객체에 channelId를 넣어놓았으므로,
/sub/channel/1을 구독하는 클라이언트에게 최종적으로 메세지를 전달할 것이다.
send 버튼을 누르자.
Subscriber
/sub/channel/1을 구독하고 있는 클라이언트에게 메세지가 전달된 것을 확인할 수 있다.
그리고 다시 우리 스프링 서버로 돌아와서 로그를 확인해보면,
메세지 전송 성공이라고 로그가 잘 찍혀있다.
로그를 확인하다 보니, 메세지를 보낼때마다, boundChannel이 달라지는 것을 확인할 수 있었다.
아마 메세지가 메세지 브로커에 전달될 때마다, 매번 새로운 내부 채널을 통해서 라우팅되는 방식으로 추정해본다.
Reference
https://brunch.co.kr/@springboot/695
https://spring.io/guides/gs/messaging-stomp-websocket/
다음 편에서는 내부 메세지 브로커를 사용한 현재 방식에서,
RabbitMQ를 사용한 외부 메세지 브로커 방식으로 구현해보겠습니다.
'Skills > Springboot Chat' 카테고리의 다른 글
Spring boot 채팅 구현 #5 RabbitMQ 기반 메시지 큐 구현 (0) | 2023.06.23 |
---|---|
Spring boot 채팅 구현 #3 AMQP 개념 정리 (0) | 2023.06.15 |
Spring boot 채팅 구현 #2 Web Socket, STOMP, Message Broker 개념 정리 (0) | 2023.06.14 |
Spring boot 채팅 구현 #1 HTTP, Polling, Long Polling 개념 정리 (0) | 2023.06.14 |