새발블로그

[Spring] WebSocket + STOMP 본문

Server/Spring

[Spring] WebSocket + STOMP

EUG 2025. 9. 22. 12:52

1. WebSocket이란?

기존 HTTP 통신의 한계를 보완하기 위한 양방향 통신 프로토콜

항목 설명
비연결성(Connectionless) 요청/응답 후 연결 종료
무상태성(Stateless) 서버는 클라이언트 상태를 기억하지 않음
단방향 통신 클라이언트가 요청해야 응답 가능 → 실시간성 부족

-> 이런 단점을 보완하는 게 바로 WebSocket

2. WebSocket 특징

특징 설명
양방향 통신 서버 ↔ 클라이언트 실시간 메시지 교환
소켓 유지 한번 연결되면 TCP 소켓이 계속 유지됨
포트 공유 HTTP와 동일한 80/443 포트 사용 (ws://, wss://)
업그레이드 방식 최초 연결은 HTTP → Upgrade: websocket 헤더로 전환

3. STOMP 통신 구조 (Pub/Sub 모델)

  • STOMP: WebSocket 위에서 동작하는 텍스트 기반 메시징 프로토콜
  • Broker: 메시지를 중개해서 구독자에게 전달
  • Spring: 내장 SimpleBroker 지원, 확장 시 RabbitMQ/Kafka 등 사용 가능

개념 정리

  • Publisher: 메시지를 보내는 주체 (예: 채팅 입력한 사용자)
  • Subscriber: 특정 토픽을 구독하고 메시지를 수신하는 클라이언트
  • Broker: 메시지를 받아 구독자들에게 전달
  • Topic: 채널 이름, /topic/… 형태

4. Spring 구성 흐름

WebSocket 설정

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic"); // 구독 prefix
    config.setApplicationDestinationPrefixes("/app"); // 발행 prefix
  }

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/chat-app").setAllowedOrigins("*");
    // ws://localhost:8080/chat-app 접속
  }
}
 

Controller

@Controller
public class ChatController {

  @MessageMapping("/hello")
  @SendTo("/topic/greetings")
  public GreetingMessage greeting(GreetingMessage message) {
      return message;
  }

  @MessageMapping("/chat")
  @SendTo("/topic/chat")
  public ChatMessage chat(ChatMessage message) {
      return message;
  }
}

 

DTO

@Data @NoArgsConstructor @AllArgsConstructor
public class GreetingMessage {
    private String name;
}

@Data @NoArgsConstructor @AllArgsConstructor
public class ChatMessage {
    private String name;
    private String content;
}

 

5. 클라이언트 (JS)

stomp.js 연결

<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>

 

연결 코드

const stompClient = new StompJs.Client({
  brokerURL: 'ws://localhost:8080/chat-app'
});

stompClient.onConnect = (frame) => {
  stompClient.subscribe('/topic/greetings', (greeting) => {
    const msg = JSON.parse(greeting.body);
    showMessage(`${msg.name}님이 입장했습니다.`);
  });

  stompClient.subscribe('/topic/chat', (chat) => {
    const msg = JSON.parse(chat.body);
    showMessage(`${msg.name}: ${msg.content}`);
  });

  stompClient.publish({
    destination: '/app/hello',
    body: JSON.stringify({name: document.getElementById('name').value})
  });
};

 

메시지 전송

function sendMessage() {
  const name = document.getElementById('name').value;
  const content = document.getElementById('content').value;

  stompClient.publish({
    destination: '/app/chat',
    body: JSON.stringify({ name, content })
  });
}
 

6. 전체 통신 흐름 요약

  1. 클라이언트가 /chat-app으로 WebSocket 연결
  2. 구독자 → /topic/... 구독
  3. 발행자 → /app/... 경로로 메시지 전송
  4. 서버 Controller에서 처리 후 @SendTo 경로로 브로드캐스트
  5. 구독 중인 클라이언트들이 메시지 수신

7. 장점

  • 채팅, 알림, 주식 시세 등 실시간 기능 쉽게 구현
  • HTTP Polling 대비 효율적
  • Spring에서 설정이 단순 (@EnableWebSocketMessageBroker 한 줄)
  • STOMP 덕분에 Pub/Sub 모델 명확

8. STOMP Trouble Shooting

1. CORS 문제

  • 증상: JS WebSocket 연결 시 403 Forbidden
  • 원인: setAllowedOrigins("*") 누락
  • 해결:
registry.addEndpoint("/chat-app").setAllowedOrigins("*");

 

2. 연결은 되는데 메시지가 안 옴

  • 원인 1: @MessageMapping 경로와 JS destination 불일치
  • 원인 2: /app prefix(발행), /topic prefix(구독) 혼동
  • 체크:
    • 발행용: /app/...
    • 구독용: /topic/...

3. HTTPS 환경에서만 연결 실패

  • 원인: ws://는 HTTPS 환경에서 차단
  • 해결: wss://(보안 WebSocket) 사용

4. 메시지 유실/끊김

  • 원인: 내장 SimpleBroker는 대규모 트래픽에 취약
  • 해결: RabbitMQ, Kafka 같은 외부 Broker 사용

5.  클라이언트 오류 감지 안 됨

  • 원인: onStompError 콜백 미등록
  • 해결:
stompClient.onStompError = (frame) => {
  console.error("Broker error: " + frame.headers['message']);
};