어제 오늘 내일

[Spring Boot] "새로고침 없이 알림이 오네요?" WebSocket과 STOMP로 실시간 채팅방 만들기 본문

IT/SpringBoot

[Spring Boot] "새로고침 없이 알림이 오네요?" WebSocket과 STOMP로 실시간 채팅방 만들기

hi.anna 2026. 3. 30. 01:58

 

카카오톡, 슬랙, 주식 시세창의 공통점은 무엇일까요?
내가 가만히 있어도 새로운 정보가 화면에 뿅! 하고 나타난다는 것입니다.

HTTP가 무전기(단방향)라면, WebSocket은 전화기(양방향)입니다.
오늘은 스프링 부트에서 이 웹소켓을 이용해 간단한 실시간 채팅방을 구현해 보겠습니다.


1. WebSocket만 쓰면 안 되나요? (STOMP의 필요성)

웹소켓은 그냥 "통신 파이프"만 뚫어줄 뿐입니다.
빨대만 꽂아놓고 "안녕?"이라고 보내면, 서버는 이게 귓속말인지, 전체 공지인지, 채팅방 1번인지 알 방법이 없습니다.

그래서 우리는 규칙(프로토콜)이 필요합니다. 그게 바로 STOMP (Simple Text Oriented Messaging Protocol)입니다.
STOMP는 "주소(Topic)" 개념을 도입해서 메시지를 배달해 줍니다.

  • Pub (발행): "1번 채팅방(/topic/room/1)에 메시지 보내줘!"
  • Sub (구독): "나 1번 채팅방(/topic/room/1) 듣고 있을게!"

2. 설정하기 (의존성 추가)

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
}

3. WebSocket 설정 (WebSocketConfig)

"어디로 연결해야 대화가 시작되는지(Endpoint)"와 "메시지 브로커(우체부)"를 설정합니다.

@Configuration
@EnableWebSocketMessageBroker // ★ STOMP 활성화
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 1. 연결 주소: ws://localhost:8080/ws-chat
        registry.addEndpoint("/ws-chat")
                .setAllowedOriginPatterns("*") // CORS 허용
                .withSockJS(); // 구형 브라우저 지원 (필수!)
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 2. 메시지 구독 요청(Sub) 주소 접두사
        // 클라이언트가 "/sub/chat/room/1"을 구독하면 브로커가 잡아서 처리함
        registry.enableSimpleBroker("/sub");

        // 3. 메시지 발행 요청(Pub) 주소 접두사
        // 클라이언트가 "/pub/chat/message"로 보내면 @MessageMapping이 잡아서 처리함
        registry.setApplicationDestinationPrefixes("/pub");
    }
}

4. 메시지 DTO 만들기

주고받을 편지지를 정의합니다.

@Getter
@Setter
public class ChatMessage {
    public enum MessageType {
        ENTER, TALK
    }
    private MessageType type; // 입장인지, 대화인지
    private String roomId;    // 방 번호
    private String sender;    // 보낸 사람
    private String message;   // 내용
}

5. 컨트롤러 만들기 (ChatController)

@RequestMapping 대신 @MessageMapping을 씁니다. HTTP 컨트롤러랑 비슷해서 아주 쉽습니다.

@Controller
@RequiredArgsConstructor
public class ChatController {

    private final SimpMessageSendingOperations messagingTemplate; // 메시지 보내는 도구

    // 클라이언트가 "/pub/chat/message"로 보내면 이 메서드가 실행됨
    @MessageMapping("/chat/message")
    public void message(ChatMessage message) {

        if (ChatMessage.MessageType.ENTER.equals(message.getType())) {
            message.setMessage(message.getSender() + "님이 입장하셨습니다.");
        }

        // ★ 핵심: "/sub/chat/room/{roomId}"를 구독 중인 사람들에게 다 뿌리기!
        messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
    }
}

6. 프론트엔드 (JavaScript) 맛보기

서버는 준비됐습니다. 클라이언트는 SockJSStomp.js 라이브러리를 써서 연결합니다.

// 1. 연결
var sock = new SockJS("/ws-chat");
var ws = Stomp.over(sock);

// 2. 접속 시도
ws.connect({}, function(frame) {
    // 3. 구독 (Subscribe) - 1번 방
    ws.subscribe("/sub/chat/room/1", function(message) {
        var recv = JSON.parse(message.body);
        console.log("받은 메시지: " + recv.message);
    });

    // 4. 메시지 전송 (Publish)
    ws.send("/pub/chat/message", {}, JSON.stringify({
        type: 'TALK', 
        roomId: '1', 
        sender: '홍길동', 
        message: '안녕하세요!'
    }));
});

이제 홍길동이 메시지를 보내면, 1번 방을 구독하고 있는 모든 사람(철수, 영희)의 콘솔창에 "안녕하세요!"가 동시에 뜹니다. 새로고침 없이요!


7. 더 나아가기 (External Broker)

스프링 부트 내장 브로커(SimpleBroker)는 메모리 기반이라서, 서버가 여러 대(Scale-out)가 되면 1번 서버에 붙은 홍길동의 말을 2번 서버의 영희가 못 듣습니다.

이때는 RabbitMQKafka 같은 외부 브로커(External Broker)를 붙여서 메시지를 공유해야 합니다. (이게 바로 대규모 채팅 시스템의 핵심입니다.)


마치며

오늘의 결론입니다.

  1. WebSocket은 실시간 양방향 통신을 가능하게 한다.
  2. STOMP를 쓰면 "구독(Sub)"과 "발행(Pub)" 구조로 쉽게 채팅방을 만들 수 있다.
  3. @MessageMapping을 통해 HTTP 컨트롤러처럼 익숙하게 개발할 수 있다.

 

 

반응형
Comments