1. STOMP(Simple or Streaming Text Oriented Messaging Protocol)
💡 STOMP(Simple or Streaming Text Oriented Messaging Protocol)
- 텍스트 기반의 메시징 프로토콜을 의미합니다. 클라이언트와 메시지 브로커 간의 통신을 간단하고 효율적으로 수행할 수 있도록 설계되었습니다. 이는 WebScoket에서 쉽게 메시지를 주고받을 때 사용이 됩니다. - 해당 프로토콜은 Websocket을 사용하여 클라이언트와 서버 간의 메시지 교환을 구조화하고 표준화하는 데 사용됩니다.
💡 WebSocketConfig 클래스 구성 - WebSocket 최초 연결을 위해 구성하는 환경 구성 파일 클래스입니다.
1. @EnableWebSocket
- WebSocket 사용을 활성화하고 @Configuration 어노테이션을 통해 환경파일임을 지정합니다. - 이 어노테이션을 사용하면 Spring 애플리케이션에서 WebSocket 기능을 사용할 수 있습니다.
2. WebSocketConfigurer(interface)
- WebSocketConfigurer 인터페이스로부터 구현체 registerWebSocketHandlers 메서드를 구성합니다. - 이 인터페이스를 구현하면 registerWebSocketHandlers 메서드를 통해 WebSocket 핸들러를 등록할 수 있습니다.
3. registerWebSocketHandlers()
- WebSocketConfigurer 인터페이스로부터 오버라이딩 받은 WebSocket 핸들러를 구성합니다. - 클라이언트에서 /ws-stomp 경로로 WebSocket 연결을 시도하면 ChatWebSocketHandler으로 연결을 처리하게 핸들러를 등록합니다.
package com.adjh.springbootwebsocket.config;
import com.adjh.springbootwebsocket.config.handler.ChatWebSocketHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* Spring Framework에서 WebSocket 구성을 위한 클래스입니다.
*
* @author : jonghoon
* @fileName : WebSocketConfig
* @since : 8/15/24
*/
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final ChatWebSocketHandler chatWebSocketHandler;
/**
* WebSocket 연결을 위해서 Handler를 구성합니다.
*
* @param registry
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
System.out.println("[+] 최초 WebSocket 연결을 위한 등록 Handler");
registry
// 클라이언트에서 웹 소켓 연결을 위해 "ws-stomp"라는 엔드포인트로 연결을 시도하면 ChatWebSocketHandler 클래스에서 이를 처리합니다.
.addHandler(chatWebSocketHandler, "ws-stomp")
// 접속 시도하는 모든 도메인 또는 IP에서 WebSocket 연결을 허용합니다.
.setAllowedOrigins("*");
}
}
5. ChatWebSocketHandler 클래스 구성
💡 ChatWebSocketHandler 클래스 구성
- WebSocket 연결 이후 연결을 처리하는 핸들러를 의미합니다. - TextWebSocketHandler를 상속받아서 최초 소켓 세션을 연결하고 소켓/전송 오류가 발생했을 때 및 ‘텍스트 기반 메시지’를 보내거나 받을 수 있도록 처리를 가능하게 합니다.
1. afterConnectionEstablished() : 연결 성공
- WebSocket 협상이 성공적으로 완료되고 WebSocket 연결이 열려 사용할 준비가 된 후 호출됩니다. 성공을 하였을 경우 session 값을 추가합니다.
2. handleTextMessage() : 메시지 전달
- 새로운 WebSocket 메시지가 도착했을 때 호출됩니다.전달 받은 메시지를 순회하면서 메시지를 전송합니다. - message.getPayload()를 통해 메시지가 전달이 됩니다.
3. afterConnectionClosed() : 소켓 종료 및 전송 오류
- WebSocket 연결이 어느 쪽에서든 종료되거나 전송 오류가 발생한 후 호출됩니다. - 종료 및 실패하였을 경우 해당 세션을 제거합니다.
package com.adjh.springbootwebsocket.config.handler;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 텍스트 기반의 WebSocket 메시지를 처리를 수행하는 Handler 입니다.
*
* @author : jonghoon
* @fileName : ChatWebSocketHandler
* @since : 8/15/24
*/
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
// WebSocket Session들을 관리하는 리스트입니다.
private static final ConcurrentHashMap<String, WebSocketSession> clientSession = new ConcurrentHashMap<>();
/**
* [연결 성공] WebSocket 협상이 성공적으로 완료되고 WebSocket 연결이 열려 사용할 준비가 된 후 호출됩니다.
* - 성공을 하였을 경우 session 값을 추가합니다.
*
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("[+] afterConnectionEstablished :: " + session.getId());
clientSession.put(session.getId(), session);
}
/**
* [메시지 전달] 새로운 WebSocket 메시지가 도착했을 때 호출됩니다.
* - 전달 받은 메시지를 순회하면서 메시지를 전송합니다.
* - message.getPayload()를 통해 메시지가 전달이 됩니다.
*
* @param session
* @param message
* @throws Exception
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println("[+] handleTextMessage :: " + session);
System.out.println("[+] handleTextMessage :: " + message.getPayload());
clientSession.forEach((key, value) -> {
System.out.println("key :: " + key + " value :: " + value);
if (!key.equals(session.getId())) { //같은 아이디가 아니면 메시지를 전달합니다.
try {
value.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
/**
* [소켓 종료 및 전송 오류] WebSocket 연결이 어느 쪽에서든 종료되거나 전송 오류가 발생한 후 호출됩니다.
* - 종료 및 실패하였을 경우 해당 세션을 제거합니다.
*
* @param session
* @param status
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws IOException {
clientSession.remove(session);
System.out.println("[+] afterConnectionClosed - Session: " + session.getId() + ", CloseStatus: " + status);
}
}
- 메시지 브로커를 구성하는 메서드로, 메시지 브로커가 특정 목적지로 메시지를 라우팅 하는 방식을 설정합니다. - enableSimpleBroker() 메서드를 통해 접두사를 지정하여 클라이언트가 접두사로 시작하는 주제를 “구독(Sub)”하여 메시지를 받을 수 있습니다. - setApplicationDestinationPrefixes() 메서드를 통해 접두사로 시작하는 클라이언트가 서버로 메시지를 “발행(Sub)” 이 접두사를 사용합니다.
4. registerStompEndpoints()
- STOMP(WebSocket 메시지 브로커 프로토콜) 엔드포인트를 등록하는 메서드로, 클라이언트가 WebSocket에 연결할 수 있는 엔드포인트를 정의합니다.
- addEndpoint() : 클라이언트가 WebSocket에 연결하기 위한 엔드포인트를 "/ws-stomp"로 설정합니다. - setAllowedOrigins() : 클라이언트의 origin을 명시적으로 지정합니다. - withSockJS() :WebSocket을 지원하지 않는 브라우저에서도 SockJS를 통해 WebSocket 기능을 사용할 수 있게 합니다.
package com.adjh.springbootwebsocket.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;
/**
* STOMP를 사용하여 메시지 브로커를 설정합니다
* WebSocket 메시지 브로커의 설정을 정의하는 메서드들을 제공합니다. 이를 통해 메시지 브로커를 구성하고 STOMP 엔드포인트를 등록할 수 있습니다.
*
* @author : jonghoon
* @fileName : WebSocketStompBrokerConfig
* @since : 8/15/24
*/
@Configuration // 설정 클래스로 지정합니다.
@EnableWebSocketMessageBroker // WebSocket 메시지 브로커를 활성화합니다.
public class WebSocketStompBrokerConfig implements WebSocketMessageBrokerConfigurer {
/**
* configureMessageBroker() : 메시지 브로커 옵션을 구성합니다.
*
* @param config
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 구독(sub) : 접두사로 시작하는 메시지를 브로커가 처리하도록 설정합니다. 클라이언트는 이 접두사로 시작하는 주제를 구독하여 메시지를 받을 수 있습니다.
// 예를 들어, 소켓 통신에서 사용자가 특정 메시지를 받기위해 "/sub"이라는 prefix 기반 메시지 수신을 위해 Subscribe합니다.
config.enableSimpleBroker("/sub");
// 발행(pub) : 접두사로 시작하는 메시지는 @MessageMapping이 달린 메서드로 라우팅됩니다. 클라이언트가 서버로 메시지를 보낼 때 이 접두사를 사용합니다.
// 예를 들어, 소켓 통신에서 사용자가 특정 메시지를 전송하기 위해 "/pub"라는 prefix 기반 메시지 전송을 위해 Publish 합니다.
config.setApplicationDestinationPrefixes("/pub");
}
/**
* registerStompEndpoints() : 각각 특정 URL에 매핑되는 STOMP 엔드포인트를 등록하고, 선택적으로 SockJS 폴백 옵션을 활성화하고 구성합니다.
*
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/*
* addEndpoint : 클라이언트가 WebSocket에 연결하기 위한 엔드포인트를 "/ws-stomp"로 설정합니다.
* withSockJS : WebSocket을 지원하지 않는 브라우저에서도 SockJS를 통해 WebSocket 기능을 사용할 수 있게 합니다.
*/
registry
// 클라이언트가 WebSocket에 연결하기 위한 엔드포인트를 "/ws-stomp"로 설정합니다.
.addEndpoint("/ws-stomp")
// 클라이언트의 origin을 명시적으로 지정
.setAllowedOrigins("<http://localhost:3000>")
// WebSocket을 지원하지 않는 브라우저에서도 SockJS를 통해 WebSocket 기능을 사용할 수 있게 합니다.
.withSockJS();
}
}
[ 더 알아보기 ] 💡 WebSocketConfig에서 지정한 엔드포인트와 WebSocketStompBrokerConfig에서 지정한 엔드포인트는 무슨 차이가 있는가? 1. WebSocketConfig 엔드포인트
- 기본적인 WebSocket 연결을 위한 것입니다. 이는 클라이언트가 최초로 WebSocket 연결을 설정할 때 사용하는 URL입니다.
2. WebSocketStompBrokerConfig 엔드포인트
- STOMP 프로토콜을 사용하는 WebSocket 연결을 위한 것입니다. 이 엔드포인트는 STOMP 클라이언트가 서버와 STOMP 세션을 설정하기 위해 사용합니다.
💡 그럼 최초 연결을 위해서는 WebSocketConfig 엔드포인트를 이용하고, 구독/발행을 할 때는 WebSocketStompBrokerConfig의 엔드포인트를 이용하는 게 맞는 걸까?
- 맞습니다. 최초 WebSocket 연결을 위해서는 WebSocketConfig 엔드포인트를 이용하고, 구독/발행 시에는 WebSocketStompBrokerConfig에서 정의한 엔드포인트와 정의한 접두사 (/sub, /pub)을 사용합니다.
7. ChatMessageDto
💡 ChatMessageDto
- 클라이언트와 데이터를 주고 받기 위해 구성한 DTO입니다. - 지역 변수로 content 값을 통해 메시지를 구성하며, sender를 통해서 보내는 주체를 지정하였습니다.
package com.adjh.springbootwebsocket.dto;
import lombok.Data;
/**
* 구독자와 수신자 간의 메시지를 주고받는 형태를 구성한 Object입니다.
*
* @author : jonghoon
* @fileName : ChatMessage
* @since : 8/16/24
*/
@Data
public class ChatMessageDto {
private String content;
private String sender;
public ChatMessageDto(String content, String sender) {
this.content = content;
this.sender = sender;
}
}
8. ChatController
💡 ChatController
- 해당 Controller에서는 STOMP를 사용하여 메시지를 처리하는 컨트롤러입니다. - @MessageMapping를 통해서 메시지로 특정 경로로 들어오는 메시지를 처리합니다.
구분
설명
엔드포인트
발행자(Publisher)
메시지를 전송하는 역할을 수행합니다
/pub/messages
구독자(Subscriber)
메시지를 수신하는 역할을 수행합니다
/sub/message
package com.adjh.springbootwebsocket.controller;
import com.adjh.springbootwebsocket.dto.ChatMessageDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* WebSocket 데이터를 처리를 수행한 Controller입니다.
*
* @author : jonghoon
* @fileName : ChatController
* @since : 8/15/24
*/
@RestController
public class ChatController {
private final SimpMessagingTemplate template; // 특정 사용자에게 메시지를 보내는데 사용되는 STOMP을 이용한 템플릿입니다.
@Autowired
public ChatController(SimpMessagingTemplate template) {
this.template = template;
}
/**
* Message 엔드포인트로 데이터와 함께 호출을 하면 "/sub/message"를 수신하는 사용자에게 메시지를 전달합니다.
*
* @param chatMessageDto
* @return
*/
@MessageMapping("/messages")
public ChatMessageDto send2(@RequestBody ChatMessageDto chatMessageDto) {
template.convertAndSend("/sub/message", chatMessageDto.getContent()); // 구독중인 모든 사용자에게 메시지를 전달합니다.
return chatMessageDto;
}
}
4) 결과 확인 : 소켓 서버 연결(Chrome Extension)
1. 소켓 서버 연결
💡 소켓 서버 연결
- 지정한 Endpoint로 접속하여 소켓 서버의 연결을 확인하였습니다. - 해당 코드에서는 ws://localhost:8081/ws-stomp로 연결을 수행하였습니다.
2. 소켓 서버 간 통신
💡 소켓 서버 간 통신
- 해당 Google Extenstion 내에서는 Message Text라는 기능이 있어서, 두 개의 브라우저 간에서 각각 세션아이디가 다르기에 다른 세션 간의 메시지가 서로 통신이 됨을 확인하였습니다.
3. 소켓 서버 내 콘솔 확인
💡 소켓 서버 내 콘솔 확인
- 소켓 서버 내에서 ChatWebSocketHandler 클래스 내에 구현한 handleTextMessage() 메서드의 작성해 둔 콘솔 메시지를 통해서 아래와 같이 출력이 잘됨을 확인하였습니다.
4. 구성한 React 앱 내에서 직접 메시지를 전송합니다.
💡 구성한 React 앱 내에서 직접 메시지를 전송합니다.
- 위에서 구성한 웹 소켓 서버는 아래의 엔드포인트로 호출을 통해 데이터를 실시간으로 주고받을 수 있습니다.