SOFTMOA TECHNOLOGY

Spring WebSocket과 STOMP로 실시간 채팅 구현

소프트모아가 정리한 기술 기록입니다.

Contents
작성일 2026. 02. 11.

개요

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 같은 외부 메시지 브로커를 사용하여 수평 확장을 고려해야 합니다.

Share this

Spring

Have a questions?

견적 및 기술문의

mobile : 010-7931-4813

Contact Form