코딩/sparta TIL

WebSocket, STOMP

americanoallday 2025. 6. 2. 19:31

✅ 1. WebSocket이란?

🔷 HTTP와의 차이부터 이해해보자

항목 HTTP WebSocket
연결 방식 요청 → 응답만 1회 양방향 연결 유지
서버 → 클라이언트 푸시 ❌ 불가능 ✅ 가능
실시간성 ❌ 느림 ✅ 빠름

 

📌 예시:

  • HTTP: 클라이언트가 서버에 요청하고, 응답 오면 끝. → 다시 요청해야 새 메시지 받음.
  • WebSocket: 클라이언트와 서버가 한번 연결되면 계속 이어짐 → 서버가 실시간으로 메시지를 보낼 수 있음!

🔁 WebSocket 흐름

  1. 브라우저가 서버에 “WebSocket 열어줘!” 요청
  2. 서버가 “좋아, 열어줄게” 하고 연결 승인
  3. 그 다음부터는 클라이언트 <=> 서버 양방향 통신 가능
  4. 채팅 메시지를 바로바로 주고받을 수 있음 (지연 거의 없음)

✅ 2. STOMP란?

WebSocket은 “통신 파이프”고, STOMP는 그 안에서 메시지를 어떤 규칙으로 주고받을지 정의한 프로토콜.

 


🔷 STOMP 용어 요약

용어 설명
@MessageMapping 클라이언트가 어디로 메시지를 보내는지 설정
@SendTo / /sub/... 서버가 어디로 메시지를 전송(브로드캐스트) 하는지 설정
@Payload 실제로 주고받는 메시지 내용
subscribe 클라이언트가 서버의 메시지를 구독
send 클라이언트가 서버에 메시지를 보냄

 


✅ 이 프로젝트에서는 어떻게 쓸까?

📦 사용 목적

구성 요소 역할
WebSocket 클라이언트와 서버 간 연결 유지
STOMP 메시지를 채팅방 별로 구분하고 전송함 (/pub/chat/message, /sub/chat/room/{roomId} 등)

 


✅ 흐름 예시 (채팅방 1번에 메시지 보내기)

  1. 클라이언트 → 서버로 메시지 보냄:
  2. stomp.send("/pub/chat/message", {}, { roomId:1, message:"안녕" })
  3. 서버 → 같은 채팅방을 구독 중인 모든 사람에게 브로드캐스트:
  4. @SendTo("/sub/chat/room/1")

✅ 요약

항목 설명
WebSocket 실시간 연결을 위한 통신 기술
STOMP 그 위에서 메시지를 잘 주고받기 위한 약속/규칙
우리는 WebSocket + STOMP를 Spring에서 함께 사용해서 실시간 채팅 구현 중

 


WebSocketConfig 설정하기

package com.example.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;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		// 클라이언트가 연결할 엔드포인트 (SockJS 지원)
		registry.addEndpoint("/ws-chat")
			.setAllowedOriginPatterns("*")
			.withSockJS();
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		// pub은 클라이언트 -> 서버 전송 prefix
		registry.setApplicationDestinationPrefixes("/pub");

		// sub은 서버 -> 클라이언트 브로드캐스트 prefix
		registry.enableSimpleBroker("/sub");
	}
}

 

ChatController 설정하기

package com.example.chat.controller;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;

import com.example.chat.dto.request.ChatMessageDto;

import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
public class ChatController {
	
	private final SimpMessagingTemplate messagingTemplate;

	@MessageMapping("/chat/message") // /pub/chat/message
	public void sendMessage(@Payload ChatMessageDto chatMessageDto) {
		String roomId = "/sub/chat/room" + chatMessageDto.getRoomId();
		messagingTemplate.convertAndSend(roomId, chatMessageDto);
	}
}

 

WebSocketMessageBrokerConfigurer란?

더보기

WebSocketConfig 클래스는 WebSocketMessageBrokerConfigurer를 구현하고 있음.

 


✅ 1. WebSocketMessageBrokerConfigurer란?

Spring에서 WebSocket + STOMP 통신을 설정할 수 있게 해주는 인터페이스.

이 인터페이스에는 여러 메서드들이 존재하는데, 전부 다 구현할 필요는 없음.

원하는 기능만 오버라이드해서 설정하면 됨.


🔧 자주 쓰는 메서드들 정리

메서드 역할 사용 여부
registerStompEndpoints() 클라이언트가 접속할 WebSocket 엔드포인트 등록 ✅ 사용함
configureMessageBroker() 메시지 브로커(prefix) 설정 (pub/sub 경로) ✅ 사용함
configureClientInboundChannel() 클라이언트 → 서버 메시지 처리 커스터마이징 ❌ 안 씀 (필요 시 필터 추가 등)
configureClientOutboundChannel() 서버 → 클라이언트 메시지 커스터마이징
addArgumentResolvers() @MessageMapping에 들어오는 파라미터 분석기 설정
configureWebSocketTransport() 메시지 크기, 버퍼 설정 등

→ 지금은 가장 핵심 2개만 사용한 것이야. 나머지는 고급 기능이 필요할 때만 설정해.


✅ 2. registerStompEndpoints() 메서드 설명

📌 이건 뭐 하는 함수?

클라이언트가 실제로 WebSocket 연결을 시도할 주소(/ws-chat) 를 지정하는 메서드야.

registry.addEndpoint("/ws-chat")
즉, 클라이언트는 ws://localhost:8080/ws-chat 으로 연결을 시도하게 됨!

📌 그 다음 .setAllowedOriginPatterns("*") 이건 뭐야?

이건 CORS(Cross-Origin Resource Sharing) 관련 설정.

  • 기본적으로 브라우저는 다른 도메인 간의 WebSocket 연결을 차단하려고 함.
  • 이걸 풀어주지 않으면, 프론트가 연결을 못 함.
  • "*"로 설정하면 모든 출처에서 연결 가능하도록 허용해주는 것.
예: localhost:3000 프론트가 localhost:8080/ws-chat으로 연결하려하면 막히는데, 이 설정이 있으면 허용됨!

📌 마지막 .withSockJS() 이건 뭔데?

SockJS는 **WebSocket이 안 되는 환경에서도 fallback(대체 통신)**을 가능하게 해주는 기술.

예: WebSocket을 지원하지 않는 브라우저 (옛날 IE 등)에서는 대신 HTTP 롱 폴링이나 AJAX로 대체해서 동작하게 함

그래서 withSockJS()를 쓰면:

  • 브라우저가 WebSocket 지원하면 WebSocket을 씀
  • 안 되면 자동으로 fallback 방식으로 대체해줌
개발/테스트 환경에서는 안정성을 위해 넣는 게 일반적

 

✅ 결과 요약: 왜 이 2개만 설정했는가?

설정 메서드이유
registerStompEndpoints() 클라이언트 연결 주소(/ws-chat)와 fallback(SockJS)을 설정하기 위해
configureMessageBroker() 메시지를 주고받을 pub/sub 주소 prefix를 설정하기 위해
나머지 고급 기능 or 성능 튜닝 단계에서 필요하므로 지금은 생략 가능

 

  • 나중에 configureClientInboundChannel() 같은 메서드를 이용해서 JWT 필터를 끼워넣을 수도 있고,
  • 메시지 사이즈나 보낼 수 있는 최대 길이 등을 configureWebSocketTransport()에서 조절할 수도 있음.

✅ 1. configureMessageBroker() 메서드의 의미

이 메서드는 STOMP 메시지를 어떤 경로로 주고받을지 규칙을 설정하는 곳.

public void configureMessageBroker(MessageBrokerRegistry registry) {
    registry.setApplicationDestinationPrefixes("/pub"); // ①
    registry.enableSimpleBroker("/sub");                // ②
}

 

 

🔹 ① setApplicationDestinationPrefixes("/pub")

  • 클라이언트가 서버에게 “보낼 때” 사용하는 경로 prefix.
  • @MessageMapping("/chat/message")는 내부적으로 /pub/chat/message에 매핑.
  • 즉, 클라이언트가 이 경로로 메시지를 보내면, 서버는 해당 컨트롤러 메서드로 처리함.
예: 클라이언트가 /pub/chat/message로 보내면 → 서버의 @MessageMapping("/chat/message") 메서드가 처리함.

 


🔹 ② enableSimpleBroker("/sub")

  • 서버가 클라이언트에게 **“보내주는 쪽 경로(prefix)”**.
  • 클라이언트는 /sub/... 경로를 구독(subscribe)하면 거기로 메시지를 받게 됨.
예: 서버가 /sub/chat/room에 메시지를 보내면 → /sub/chat/room을 구독 중인 모든 클라이언트가 메시지를 받음.

✅ 2. 이걸 실제로 쓰는 코드 예시 분석

@MessageMapping("/chat/message")     // ①
@SendTo("/sub/chat/room")            // ②
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) { // ③
    return chatMessage;
}

 

🔹 ① @MessageMapping("/chat/message")

  • 클라이언트가 /pub/chat/message로 메시지를 보내면 이 메서드가 실행됨
  • /pub은 위에서 지정한 prefix,
  • /chat/message메서드의 STOMP 주소

🔹 ② @SendTo("/sub/chat/room")

  • 이 메서드가 리턴한 값을, /sub/chat/room구독 중인 모든 클라이언트에게 전송함
즉, 서버가 메시지를 브로드캐스트하는 경로

🔹 ③ @Payload

  • 클라이언트가 보낸 **메시지 본문(json)**을 자동으로 ChatMessage 객체로 변환
  • @RequestBody와 비슷한 역할(WebSocket용)

 


✅ 정리: 클라이언트 입장에서 흐름

단계 설명
1. 연결 ws://localhost:8080/chat 으로 연결 (엔드포인트)
2. 구독 stomp.subscribe("/sub/chat/room")
3. 메시지 보냄 stomp.send("/pub/chat/message", {}, JSON.stringify({...}))
4. 서버 처리 @MessageMapping("/chat/message") 메서드 실행
5. 메시지 전송 @SendTo("/sub/chat/room")로 메시지 전송됨

 


✅ 추가로 해줄 것

  • 엔드포인트 /chat으로 바꾸기:
registry.addEndpoint("/chat")
  .setAllowedOriginPatterns("*")
  .withSockJS();

 


필요하면 이 흐름에 맞는 클라이언트 JS 코드나 테스트 HTML도 만들어줄게.

다음으로 가볼까? Redis 연동 or 메시지 저장 or 프론트 테스트 중 어떤 걸 하고 싶어?

 

SimpMessagingTemplate

더보기

WebSocket + STOMP 기반에서 “서버 → 클라이언트”로 메시지를 보낼 때 사용하는 스프링 전용 컴포넌트.


✅ 그렇다면 꼭 써야 해?

🔹 꼭 써야 하진 않지만, “상황에 따라 필수”임.

상황 필요 여부
고정된 경로로만 브로드캐스트 ❌ 안 써도 됨 (@SendTo로 처리 가능)
동적 경로로 보내야 함 (방 ID별로) 꼭 필요함
특정 사용자에게만 전송해야 함 ✅ 필요 (convertAndSendToUser)
Redis 등 외부 이벤트로 메시지 보낼 때 ✅ 필요 (컨트롤러 밖에서 메시지 전송 가능)

 


✅ 예시 비교

🔸 1) @SendTo 방식 (간단하지만 고정)

@MessageMapping("/chat/message")
@SendTo("/sub/chat/room")
public ChatMessageDto send(ChatMessageDto message) {
    return message;
}

 

장점: 간단함
단점: 방 ID마다 다른 구독 채널 못 씀 (모든 방이 /sub/chat/room에 모임)

🔸 2) SimpMessagingTemplate 방식 (유연)

messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
장점: 방 ID별 구독, 유저별 전송, 비동기 전송, 컨트롤러 외부에서도 사용 가능
단점: 코드가 조금 더 직접적 (하지만 실무에선 필수 수준)

✅ 요약

질문 답변
SimpMessagingTemplate는 언제 써? WebSocket + STOMP 기반에서 동적으로 메시지를 브로커로 보낼 때
안 써도 가능? 단순한 브로드캐스트만 한다면 가능하지만 실무에선 거의 필수
왜 써야 해? 채팅방, 유저 등 구독 경로가 유동적인 경우 대응 가능하기 때문