WebSocket + STOMP를 이용해서 단체 채팅방을 한번 구현해보자
웹 소켓 구현을 위한 클래스들
1. WebSocketConfig
2. ChatWebSocketHandler
3. WebSocketStompBrokerConfig
4. ChatMessageDto
5. ChatController
1. 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.chat.config;
import com.chat.handler.ChatWebSocketHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.*;
/**
* Spring에서 WebSocket 구성을 위한 클래스
*/
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final ChatWebSocketHandler chatWebSocketHandler;
// WebSocket 연결을 위해서 Handler을 구성
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
System.out.println("[+] 최초 WebSocket 연결을 위한 등록 Handler");
registry
// 클라이언트에서 웹 소켓 연결을 위해 "chat-room"라는 앤드포인트로 연결을 시도하면
// ChatWebSocketHandler 클래스에서 이를 처리한다.
.addHandler(chatWebSocketHandler, "chat-room")
.setAllowedOrigins("*"); // 접속 시도하는 모든 도메인 IP에서 webSocket 연경을 허용
}
}
2. ChatWebSocketHandler
- WebSocket 연결 이후 연결을 처리하는 핸들러를 의미
- TextWebSocketHandler를 상속받아서 최초 소켓 세션을 연결하고 소켓/ 전송 오류가 발생했을 때 및 텍스트 기반 메시지를 보내거나 받을 수 있도록 처리가 가능하게 함
1. afterConnectionEstablished() : 연결 성공
- WebSocket 협상이 성공적으로 완료되고 WebSocket 연결이 열려 사용할 준비가 된 후 호출됩니다. 성공을 하였을 경우 session 값을 추가합니다.
2. handleTextMessage() : 메시지 전달
- 새로운 WebSocket 메시지가 도착했을 때 호출됩니다.전달 받은 메시지를 순회하면서 메시지를 전송합니다.
- message.getPayload()를 통해 메시지가 전달이 됩니다.
3. afterConnectionClosed() : 소켓 종료 및 전송 오류
- WebSocket 연결이 어느 쪽에서든 종료되거나 전송 오류가 발생한 후 호출됩니다.
- 종료 및 실패하였을 경우 해당 세션을 제거합니다.
package com.chat.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.concurrent.ConcurrentHashMap;
/**
* 텍스트 기반의 WebSocket 메시지를 처리를 수행하는 Handler 입니다.
*/
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
// WebSocket Session들을 관리하는 리스트입니다.
private static ConcurrentHashMap<String, WebSocketSession> clientSession = new ConcurrentHashMap<>();
/**
* [연결 성공] WebSocket 협상이 성공적으로 완료되고 WebSocket 연결이 열려 사용할 준비가 된 후 호출합니다.
* - 성공을 하였을 경우 session 값을 추가합니다.
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("[*] afterConnectionEstablished ::" + session.getId());
clientSession.put(session.getId(), session);
}
/**
* [메시지 전달] 새로운 WebSocket 메시지가 도착했을 때 호출됩니다.
* - 전달 받은 메시지를 순회하면서 메시지를 전송합니다.
* - message.getPayload()를 통해 메시지가 전달이 됩니다.
*/
@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 연결이 어느 쪽에서든 종료되거나 전송오류가 발생한 후 호출됨
* - 종료 및 실패하였을 경우 해당 세션 제거
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
clientSession.remove(session.getId());
System.out.println("[*] afterConnectionClosed :: " + session.getId());
}
}
3. WebSocketStompBrokerConfig
- WebSocket 및 STOMP 메시징 처리를 구현하는 WebSocketMessageBrokerConfigurer 인터페이스 구현체
- 이를 통해 WebSocket 통신을 위한 다양한 설정을 구성할 수 있다.
1. @EnableWebSocketMessageBroker
- WebSocket 메시지 브로커를 활성화합니다. 이 애노테이션을 사용하면, 해당 클래스는 WebSocket 메시징을 위한 설정을 할 수 있습니다.
2. WebSocketMessageBrokerConfigurer
- WebSocket 연결을 처리하는 핸들러를 의미합니다. TextWebSocketHandler를 상속하여 ‘텍스트 기반 메시지’를 보내거나 받을 수 있도록 처리가 가능합니다.
3. configureMessageBroker(MessageBrokerRegistry config)
- 메시지 브로커를 구성하는 메서드로, 메시지 브로커가 특정 목적지로 메시지를 라우팅 하는 방식을 설정합니다.
- enableSimpleBroker() 메서드를 통해 접두사를 지정하여 클라이언트가 접두사로 시작하는 주제를 “구독(Sub)”하여 메시지를 받을 수 있습니다.
- setApplicationDestinationPrefixes() 메서드를 통해 접두사로 시작하는 클라이언트가 서버로 메시지를 “발행(Sub)” 이 접두사를 사용합니다.
4. registerStompEndpoints()
- STOMP(WebSocket 메시지 브로커 프로토콜) 엔드포인트를 등록하는 메서드로, 클라이언트가 WebSocket에 연결할 수 있는 엔드포인트를 정의합니다.
- addEndpoint() : 클라이언트가 WebSocket에 연결하기 위한 엔드포인트를 "/ws-stomp"로 설정합니다.
- setAllowedOrigins() : 클라이언트의 origin을 명시적으로 지정합니다.
- withSockJS() :WebSocket을 지원하지 않는 브라우저에서도 SockJS를 통해 WebSocket 기능을 사용할 수 있게 합니다.
package com.chat.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 엔트포인트를 등록할 수 있음
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompBrokerConfig implements WebSocketMessageBrokerConfigurer {
/**
* configureMessageBroker(): 메시지 브로커 옵션을 구성합니다.
*/
@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 플백 옵션을 활성화하고 구성
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// addEndpoint : 클라이언트가 WebSocket에 연결하기 위한 엔드포인트를 "/ws-stomp"로 설정
// withSockJS : WebSocket을 지원하지 않는 브라우저에서도 SocketJS를 통해 WebSocket 기능을 사용할 수 있게 합니다.
registry
// 클라이언트가 WebSocket에 연결하기 위한 엔드포인트를 "/ws-stomp"로 설정합니다.
.addEndpoint("/ws-stomp")
// 클라이언트의 origin을 명시적으로 지정
.setAllowedOrigins("http://localhost:3000","http://localhost:3001")
// WebSocket을 지원하지 않는 브라우저에서도 SockJS를 통해 WebSocket 기능을 사용
.withSockJS();
}
}
4. ChatMessageDto
- 클라이언트와 데이터를 주고 받기 위해 구성한 DTO
import java.time.LocalDateTime;
public record ChatMessageDto(
int fundingId,
int senderId,
String nickname,
String content,
String status, // ex: SENT, DELIVERED
LocalDateTime createdAt
) {}
5. ChatController
- STOMP르 사용하여 메시지를 처리하는 컨트롤러
- @MessageMapping를 통해서 메시지를 틀정 경로로 돌아오는 메시즈를 처리
발행자(Publisher)
- 메시지를 전송하는 역할을 수행
- 엔드 포인트 : /pub/chat/{fundingId}
구독자(Subscriber)
- 메시지를 수신하는 역할을 수행
- 엔드포인트: /sub/message
import com.chat.dto.ChatMessageDto;
import com.chat.repository.ChatMessageRepository;
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.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
public class ChatController {
// 특정 사용자에게 메시지를 보내는데 사용되는 STOMP을 이용한 템플릿
private final SimpMessagingTemplate template;
private final ChatMessageRepository chatMessageRepository;
// 엔드 포인트로 데이터와 함께 호출하면 "sub/chat/{fundingId}" 를 수신하는 사용자에게 메세지를 전달합니다.
@MessageMapping("/chat/{fundingId}")
public ChatMessageDto send2(@DestinationVariable(value = "fundingId") int fundingId, ChatMessageDto chatMessageDto){
log.info("MessageDto: {}", chatMessageDto);
template.convertAndSend("/sub/chat/" + fundingId , chatMessageDto);
return chatMessageDto;
}
}
React 앱 내에서 메시지를 전송
소켓 서버 연결
- 엔드포인트 : http://localhost:8087/chat-room
- 소켓 서버에 연결을 하는데 사용이 되는 엔드포인트
메시지 전송
- 엔드포인트 : /pub/chat/{fundingId}
- 메시지는 전송하기 위해 사용되는 앤드포인트
메시지 수신
- 엔드포인트 : /sub/chat/{fundingId}
- 메시지를 수신하기 위해 사용되는 엔드포인트
💡 채팅 시스템의 흐름이 어떻게 될까??
전체 적인 흐름도
[Client1] → ws://localhost:8087/chat-room (SockJS) ----------- WebSocket 연결 완료
↓
subscribe("/sub/chat/1") ← ✅ 먼저 구독
↓
send("/pub/chat/1", "안녕") → 서버
↓
서버: convertAndSend("/sub/chat/1", "안녕")
↓
[Client1] → 메시지 받음 ✅
[Client2] → 메시지 받음 ✅ (구독 중이라면)
💡 핵심 개념: pub, sub는 prefix이지, 실제 URL은 아니다!!
👉 prefix는 STOMP 메시지 경로에서 의미적인 역할을 하는 접두어
prefix의 역할
/pub : 클라이언트가 메시지를 서버로 보낼 때 사용 (→ @MessageMapping)
config.setApplicationDestinationPrefixes("/pub");
- 이건 “클라이언트가 보내는 메시지 중 /pub로 시작하는 경로는 Spring에서 처리해줄게” 라는 뜻이다.
- 즉, 클라이언트가 /pub/chat/1으로 보냈으면, Spring은 내부적으로 chat/1을 꺼내서
→ @MessageMapping("/chat/{id}")가 붙은 메서드와 매핑시킴.
@MessageMapping("/chat/{fundingId}")
public ChatMessageDto send2(@DestinationVariable(value = "fundingId") int fundingId, ChatMessageDto chatMessageDto){
/sub : 클라이언트가 서버로부터 메시지를 받을 때 사용 (→ convertAndSend)
template.convertAndSend("/sub/chat/" + fundingId , chatMessageDto);
서버는 이 구독 경로로 메시지를 브로드캐스트 한다.
- /sub/chat/1을 구독하고 있는 모든 유저에게 메시지를 보냄
브로드캐스트(Broadcast)
- 하나의 메시지를 여러 명에게 동시에 보내는 것
- 쉽게 말해, '전체 공지' 같은 개념
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompBrokerConfig implements WebSocketMessageBrokerConfigurer {
/**
* configureMessageBroker(): 메시지 브로커 옵션을 구성합니다.
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 구독(sub) : 접두사로 시작하는 메시지를 브로커가 처리하도록 설정
// 클라이언트는 이 접두사로 시작하는 주제를 구독하여 메시지를 받음
// 예) 소켓 통신에서 사용자가 특정 메시지를 받기 위해 "/sub" 이라는 prefix 기반 메시지 수신을 위해 Subscribe 함
config.enableSimpleBroker("/sub");
// 발행(pub) : 접두사로 시작하는 메시지는 @MessageMapping이 달린 메서드로 라우팅됩니다.
// 예) 소켓 통신에서 사용자가 특정 메시지를 전송하게 위해 "/pub"라는 prefix 기반 메시지 전송을 위해 Publish 합니다.
config.setApplicationDestinationPrefixes("/pub");
}
출처
이분을 도움을 많이 받았습니다. 강추드립니다.
https://adjh54.tistory.com/573
[Java] Spring Boot WebSocket + STOMP 이해하고 구성하기 -1: 초기 구성 및 간단 소켓 연결
해당 글에서는 Spring Boot 기반 WebSocket에 대해 이해하고 초기 설정 이후 WebScocket에 연결하는 방법에 대해 알아봅니다 1) Spring Boot WebSocket💡 Spring Boot WebSocket- Spring Framework에서 제공하는 기능으로
adjh54.tistory.com
'Spring' 카테고리의 다른 글
[Spring] Apache Kafka 이해하기 (0) | 2025.04.01 |
---|---|
[Spring] Spring Boot WebSocket + STOMP 구성요소 (0) | 2025.03.30 |