Contents
see List개요
WebSocket은 HTTP와 달리 양방향 통신을 지원하는 프로토콜로, 실시간 채팅, 알림, 협업 도구 등에 필수적입니다. Spring Framework는 STOMP(Simple Text Oriented Messaging Protocol)를 통해 WebSocket 위에서 메시징 패턴을 구현할 수 있도록 지원합니다. 이 문서에서는 Spring Boot 3.5 기반 실시간 채팅 시스템 구축 방법을 다룹니다.
WebSocket 설정
먼저 Spring WebSocket 의존성을 추가하고 STOMP 엔드포인트를 설정합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 클라이언트가 구독할 prefix
registry.enableSimpleBroker("/topic", "/queue");
// 클라이언트가 메시지 전송 시 사용할 prefix
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS(); // SockJS fallback 지원
}
}메시지 모델과 컨트롤러
채팅 메시지를 처리할 도메인 모델과 컨트롤러를 작성합니다.
public record ChatMessage(
String type, // JOIN, CHAT, LEAVE
String content,
String sender,
LocalDateTime timestamp
) {}
@Controller
public class ChatController {
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
return new ChatMessage(
chatMessage.type(),
chatMessage.content(),
chatMessage.sender(),
LocalDateTime.now()
);
}
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(
@Payload ChatMessage chatMessage,
SimpMessageHeaderAccessor headerAccessor
) {
// 세션에 사용자명 저장
headerAccessor.getSessionAttributes()
.put("username", chatMessage.sender());
return new ChatMessage(
"JOIN",
chatMessage.sender() + "님이 입장했습니다.",
chatMessage.sender(),
LocalDateTime.now()
);
}
}이벤트 리스너로 연결 관리
사용자 입장/퇴장 이벤트를 처리하는 리스너를 구현합니다.
@Component
@RequiredArgsConstructor
public class WebSocketEventListener {
private final SimpMessageSendingOperations messagingTemplate;
@EventListener
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
log.info("새로운 WebSocket 연결: {}", event.getMessage());
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor =
StompHeaderAccessor.wrap(event.getMessage());
String username = (String) headerAccessor
.getSessionAttributes()
.get("username");
if (username != null) {
ChatMessage chatMessage = new ChatMessage(
"LEAVE",
username + "님이 퇴장했습니다.",
username,
LocalDateTime.now()
);
messagingTemplate.convertAndSend("/topic/public", chatMessage);
}
}
}프론트엔드 연동 (JavaScript)
SockJS와 STOMP.js를 사용한 클라이언트 구현 예제입니다.
// npm install sockjs-client @stomp/stompjs
let stompClient = null;
let username = null;
function connect() {
username = document.querySelector('#username').value.trim();
const socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, onError);
}
function onConnected() {
// 채팅방 구독
stompClient.subscribe('/topic/public', onMessageReceived);
// 입장 알림
stompClient.send("/app/chat.addUser",
{},
JSON.stringify({ sender: username, type: 'JOIN' })
);
}
function sendMessage() {
const messageContent = document.querySelector('#message').value.trim();
if (messageContent && stompClient) {
const chatMessage = {
sender: username,
content: messageContent,
type: 'CHAT'
};
stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));
}
}
function onMessageReceived(payload) {
const message = JSON.parse(payload.body);
const messageElement = document.createElement('li');
if (message.type === 'JOIN') {
messageElement.classList.add('event-message');
messageElement.textContent = message.content;
} else if (message.type === 'LEAVE') {
messageElement.classList.add('event-message');
messageElement.textContent = message.content;
} else {
messageElement.classList.add('chat-message');
messageElement.textContent = message.sender + ': ' + message.content;
}
document.querySelector('#messageArea').appendChild(messageElement);
}활용 팁
- 보안 강화: Spring Security와 통합하여 JWT 기반 WebSocket 인증을 구현할 수 있습니다.
- 메시지 영속화: MongoDB나 Redis를 연동해 채팅 히스토리를 저장하고 재접속 시 로드할 수 있습니다.
- 스케일 아웃: 여러 서버 인스턴스 환경에서는 Redis Pub/Sub 또는 RabbitMQ를 External Broker로 사용해야 합니다.
- 개인 메시지: /queue/private-{userId} 형태로 구독하여 1:1 DM 기능을 구현할 수 있습니다.
- 읽음 확인: 메시지마다 고유 ID를 부여하고, 클라이언트에서 읽음 ACK를 전송하는 방식으로 구현 가능합니다.
마무리
Spring WebSocket과 STOMP는 복잡한 WebSocket 프로토콜을 추상화하여 선언적 방식으로 실시간 통신을 구현할 수 있게 합니다. 채팅뿐만 아니라 실시간 알림, 협업 편집기, 게임 서버 등 다양한 실시간 애플리케이션에 활용할 수 있습니다. 프로덕션 환경에서는 반드시 Redis나 RabbitMQ 같은 외부 메시지 브로커를 사용하여 수평 확장을 고려해야 합니다.