[Spring] Spring 과 STOMP 웹소켓 채팅 기능 구현
🎯 STOMP 란 무엇일까?
STOMP 에 대해 설명하기 전에 웹 소켓 통신에 대해 간단히 설명드리겠습니다.
웹 소켓은 클라이언트와 서버 간의 양방향 통신을 가능하게 하는 프로토콜 입니다. HTTP 와 달리 연결을 유지한 채 데이터를 주고 받을 수 있기 때문에 실시간 채팅, 주식, 게임 등에 많이 사용됩니다. HTTP 와 다른 TCP 프로토콜이지만 HTTP 에서 동작이 가능하게 디자인 되었고 80, 443 포트 를 사용해 방화벽 규칙을 재사용할 수 있도록 설계 되었습니다.
그러면 웹소켓을 사용할 때는 기본적으로 바이너리 데이터 또는 텍스트 데이터를 주고 받을 수 있지만, 메시지의 목적지를 정하거나 규칙을 정하는 기능이 부족합니다. 그래서 STOMP(Simplse Text Oriented Messaging Protocol)이라는 프로토콜을 사용하면 메시지의 목적지, 헤더, 본문 등을 명확하게 정의할 수 있습니다.
🔹 STOMP 주요 개념
- CONNECT : 클라이언트가 서버와 연결을 시작할 때 사용한다.
- SUBSCRIBE : 특정 토픽(채널)에 구독하여 메시지를 받을 때 사용한다,
- SEND : 서버로 메시지를 보낼때 사용한다.
- MESSAGE : 서버가 클라이언트에게 메시지를 보낼때 사용한다.
- DISCONNECT : 연결 종료
🎯 Spring WebSocket + STOMP 기반 채팅 흐름
그러면 Spring 에서는 STOMP 를 어떻게 사용해 웹소캣 채팅 서버를 구현할 수 있을까? 기본적인 흐름은 다음과 같습니다.
클라이언트 <-> 서버 간 흐름
1. 클라이언트가 WebSocket 연결을 생성한다.
2. 서버는 STOMP 핸드셰이크를 수행해 연결을 승인한다.
3.클라이언트가 채팅방을 구독한다.
4. 사용자가 메시지를 보내면, 서버가 이를 해당 채팅방에 전송한다.
5. 구독한 클라이언트들이 메시지를 실시간으로 받는다.
자! 그러면 위 흐름을 기반으로 채팅 서버를 구현해보겠습니다.
✅ StompWebSocketConfig
package com.chatting.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
@EnableWebSocketMessageBroker
@Configuration
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
// /dm
@Value("${domain.chat.directMessage}")
private String dmUrl;
// /pub
@Value("${domain.chat.publish}")
private String pub;
//sub
@Value("${domain.chat.subscribe}")
private String sub;
@Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
registry.addEndpoint(dmUrl)
.setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(final MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes(pub);
registry.enableSimpleBroker(sub);
}
}
애노테이션과 설정한 프로퍼티 값은 다음과 같습니다.
@EnableWebSocketMessageBroket : WebSocket 메시지 브로커를 활성화 시킵니다. STOMP 프로토콜을 기반으로 메시지 전송/ 구독 기능을 제공합니다.
@Configuration : Spring 설정 클래스로 등록하여 WebSocket 관련 설정을 적용합니다.
domain.chat.directMessage : WebSocket 연결을 받을 엔드포인트 URL
domain.chat.publish : 메시지를 발행하는 경로
domain.chat.subscribe : 클라이언트가 메시지를 구독하는 경로
StompWebSocketConfig 는 WebSocketMessageBrokerConfigurer 를 상속받고 있는데, 그 중 registerStompEndpoints 와 configureMessageBroker 를 오버라이드 했습니다.
registerStompEndpoints() 는 WebSocket 연결을 받을 엔드 포인트를 설정 하고 configureMessageBroker() 는 클라이언트가 메시지를 보낼(발행할) 경로와 구독할 경로를 지정했습니다.
자! 그러면 위에서 설명했던 클라이언트와 서버간의 양방향 통신 과정을 다시 살펴봅시다.
1. 클라이언트가 WebSocket 연결을 생성한다.
2. 서버는 STOMP 핸드셰이크를 수행해 연결을 승인한다.
3.클라이언트가 채팅방을 구독한다.
4. 사용자가 메시지를 보내면, 서버가 이를 해당 채팅방에 전송한다.
5. 구독한 클라이언트들이 메시지를 실시간으로 받는다.
즉, 클라이언트(어플 혹은 웹 사이트) 에서 WebSocket 연결을 생성하고 서버에서는 registerStompEndpoints() 로에서 핸드셰이크 수행 후 승인합니다. 클라이언트가 STOMP CONNECT 프레임을 보내면 서버가 CONNECTED 를 응답하고 이후 STOMP 프로토콜을 이용해 메시지를 주고 받습니다.
그러면 만약 두명의 사용자가 1:1 채팅을 한다고 가정해봤을때 두명의 사용자만의 채널(토픽) 이 있어야 할 겁니다. 저 같은 경우 두명의 사용자가 채팅을 시작하면 채팅 방 을 만들고 DB 테이블에 채팅방 정보를 저장합니다. 이렇게 생성된 채팅방의 PK 를 이용해 클라이언트가 채팅방을 구독하게끔 만들었습니다.
✅ 클라이언트의 WebSocket 연결 방법 (Flutter)
void connectToStomp() {
if (stompClient != null) {
return; // 이미 연결된 경우 새로 연결하지 않음
}
Map<String, String> headers = {
'roomId': _roomId.toString(),
'userId': _currentUser.id.toString(),
};
stompClient = StompClient(
config: StompConfig(
url: WEB_SOCKET_URL,
onConnect: onStompConnected,
onWebSocketError: (dynamic error) => print('WebSocket Error: $error'),
onStompError: (dynamic error) => print('Stomp Error: $error'),
onDisconnect: (frame) => print('Disconnected'),
stompConnectHeaders: headers, // 여기에 헤더 추가
),
);
stompClient?.activate();
}
저는 Flutter 로 만든 어플로 채팅 기능을 구현했습니다. STOMP 에서는 메시지를 주고 받기 위해 채널이라는 개념을 사용합니다. 이때 SUBSCRIBE 를 하면 특정 채널을 구독 하고, 해당 채널로 메시지가 오면 자동으로 받는 구조입니다.
즉, 클라이언트는 사용자를 구별할 특정 채널 을 만들어야 하고 서버가 해당 채널로 메시지를 보내면, 구독한 클라이언트가 메시지를 수신합니다.
자! 그러면 클라이언트가 특정 채널을 구독했으니, 이제 서버가 해당 채널로 메시지를 보내야 합니다. 서버에서 메시지를 보내는 방법은 다음과 같습니다.
✅ DirectMessageApiController
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping
public class DirectMessageApiController {
private final SimpMessagingTemplate template;
private final DirectMessageService dmService;
@MessageMapping(value = "/chat/message")
public void sendMessage(final DirectMessageRequest dmDto) {
DirectMessage directMessage = dmService.toDirectMessage(dmDto);
DirectMessageResponse directMessageResponse = dmService.toDirectMessageResponse(directMessage);
dmService.saveDirectMessageAndPushNotication(directMessage);
template.convertAndSend("/sub/chat/room/" + dmDto.getRoomId(), directMessageResponse);
}
@MessageMapping() 과 SimpMessagingTeplate 를 사용하면 간단하게 구현할 수 있습니다.
클라이언트는 /chat/message 로 메시지를 보내면 메시지관련 정보가 담긴 DirectMessageRequest 에서 데이터를 꺼내와 두 사용자가 포함된 채널(토픽) 에 메시지를 보냅니다.
이제 클라이언트는 /pub/chat/message 경로로 메시지를 보내면 자신의 채널에 포함된 사용자에게 실시간으로 채팅 정보를 보낼 수 있습니다.
✅ 클라이언트가 메시지를 보내는 방법(Flutter)
void sendMessage() {
var directMessageDto = DirectMessageRequest(
senderId: _sender.id,
receiverId: _receiver.id,
message: textEditingController.text,
roomId: _roomId.toString()
);
stompClient?.send(
destination: '/pub/chat/message',
body: jsonEncode(directMessageDto.toJson()),
);
}
지금까지 Spring 에서 WebSocket 를 이용한 채팅 채널을 만드는 방법에 대해 설명했습니다. 하지만 채팅 기능은 읽음/ 읽지 않음 처리, 메시지 푸쉬 알림 등 여러가지 기능이 추가되어야 합니다.
사용자가 메시지를 읽었는지 여부를 처리하는 방법은 아래 포스팅을 참고해주세요! 다음 웹소켓 관련 포스팅은 실시간 사진 전송 에 대해 설명 드리도록 하겠습니다.
https://comumu.tistory.com/141
Spring과 WebSocket으로 실시간 채팅에서 '읽음/읽지 않음' 상태 관리하기
🎯 구현할 기능아래 그림처럼 두명의 사용자 사이의 1:1 채팅 기능을 구현하고 있습니다. 웹 소켓 기능을 이용해 만들었고, 두명의 사용자가 모두 채팅방에 입장해 있으면 채팅을 읽음 으로 표
comumu.tistory.com