채팅 토이 프로젝트를 진행하면서 공부했던 STOMP 관련 내용 정리 - dnwls16071/Backend_Summary GitHub Wiki

📚 WebSocketMessageBrokerConfigurer

  • Spring Framework에서 STOMP(Simple Text Oriented Messaging Protocol) 기반의 WebSocket 메시징을 설정하기 위한 인터페이스

주요 역할 정리

  • STOMP 메시징 활성화 : WebSocketMessageBrokerConfigurer를 구현한 설정 클래스에 @EnableWebSocketMessageBroker 어노테이션을 추가하여 WebSocket을 통한 STOMP 메시지 처리를 활성화한다.
  • 엔드포인트 등록 : 클라이언트가 웹소켓 연결을 시작할 수 있는 STOMP 엔드포인트를 등록한다.
  • 메시지 브로커 구성 : 클라이언트의 SUBSCRIBE 요청을 처리하고 메시지를 전달하는 메시지 브로커를 설정한다.
  • 애플리케이션 대상 경로 설정 : 클라이언트가 서버로 메시지를 보낼 때 사용하는 목적지(destination) 접두사를 설정한다.

토이 프로젝트 시 작성했던 코드 정리

@Slf4j
@Configuration
@EnableWebSocket
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {

	private final StompHandler stompHandler;

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		// Register STOMP endpoints mapping each to a specific URL and (optionally) enabling and configuring SockJS fallback options.
		registry.addEndpoint("/connect")
				.setAllowedOrigins("http://localhost:3000")
				.withSockJS();
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		// Configure message broker options.
		// @MessageMapping이 있는 컨트롤러 메서드
		// 클라이언트 → 서버(애플리케이션)로 메시지를 보낼 때 사용되는 목적지 접두사를 설정합니다.
		registry.setApplicationDestinationPrefixes("/publish");

		// 메모리 기반의 내장 메시지 브로커
		// 서버 → 클라이언트(브로드캐스트)로 메시지를 보낼 때 사용되는 목적지 접두사를 설정합니다.
		registry.enableSimpleBroker("/topic");
	}

	@Override
	public void configureClientInboundChannel(ChannelRegistration registration) {
		// 클라이언트 메시지가 실제 핸들러에 도달하기 전에 메시지를 가로채고 처리
		// 클라이언트가 보낸 메시지에 대한 인증, 권한 부여, 로깅 등을 처리
		log.info("Interceptor - OK...");
		registration.interceptors(stompHandler);
	}
}

📚 ChannelInterceptor

  • Spring Framework의 메시징 모듈에서 메시지가 MessageChannel을 통해 전송되거나 수신될 때 이를 가로채서 처리하는 역할을 하는 인터페이스
  • 웹소켓(WebSocket)과 STOMP(Simple Text Oriented Messaging Protocol) 기반 애플리케이션에서 주로 사용되며, 메시지 전송 및 수신 과정에 공통적인 로직을 적용하는 필터와 유사한 역할을 수행한다.

주요 역할 정리

  • 메시지 가로채기 : 클라이언트가 서버로 보내는 STOMP 메시지(CONNECT, SEND, SUBSCRIBE 등)나 서버가 클라이언트로 보내는 메시지를 가로챈다. 이를 통해 메시지가 처리되기 전후로 다양한 작업을 수행할 수 있다.
  • 인증 및 권한 부여 : 웹소켓 연결 시 JWT(JSON Web Token)와 같은 인증 정보를 메시지 헤더에서 추출하여 인증 및 권한 확인 로직을 구현할 수 있다. 예를 들어, preSend() 메서드를 오버라이드하여 메시지 전송 전에 사용자의 유효성을 검사할 수 있다.
  • 로깅 및 모니터링 : 메시지 채널을 통과하는 모든 메시지를 로깅하여 디버깅이나 모니터링에 활용할 수 있다.

토이 프로젝트 시 작성했던 코드 정리

@Component
@RequiredArgsConstructor
public class StompHandler implements ChannelInterceptor {

	private final JwtDecoder jwtDecoder;

	@Override
	public Message<?> preSend(Message<?> message, MessageChannel channel) {
		// 메시지가 실제로 전송되기 전에 호출되는 사전 처리 단계를 의미
		final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

		System.out.println("StompHandler.preSend() called - Command: " + accessor.getCommand());

		if (StompCommand.CONNECT == accessor.getCommand()) {
			final String bearerToken = accessor.getFirstNativeHeader("Authorization");

			if (bearerToken == null) {
				System.out.println("No Authorization header found!");
				throw new IllegalArgumentException("Authorization header is missing");
			}

			final String token = bearerToken.substring(7);

			try {
				jwtDecoder.decode(token);
			} catch (Exception e) {
				throw new IllegalArgumentException("Invalid JWT token");
			}
		}
		return ChannelInterceptor.super.preSend(message, channel);
	}
}

📚 ChatController

관련 어노테이션/메서드/클래스

  • @MessageMapping : 웹소켓에서 클라이언트가 서버로 메시지를 보내는 경로를 지정한다. 해당 경로로 메시지를 보내면 이 메서드가 해당 메시지를 받아서 처리한다. 다시 말해 클라이언트가 메시지를 발행하는 통로 역할을 한다.
  • SimpMessagingTemplate : 서버에서 클라이언트로 메시지를 전송할 때 사용하는 핵심 컴포넌트. 서버가 메시지 브로커에게 메시지를 전달하는 역할을 수행한다.
  • @DestinationVariable : STOMP 메시지의 도착지(destination) 헤더에서 변수 값을 추출하여 roomId 매개변수에 바인딩한다. 이를 통해 서버는 클라이언트가 보낸 메시지가 어느 채팅방에 대한 것인지를 식별할 수 있게 된다.
  • convertAndSend() : 요청 객체를 적절한 메시지 형식으로 변환하여 전송한다.

토이 프로젝트 시 작성했던 코드 정리

@Controller
@RequiredArgsConstructor
public class ChatController {

	private final SimpMessagingTemplate messagingTemplate;

	// 클라이언트로부터 메시지를 수신하고 처리합니다.
	// /publish/chat/{roomId}로 메시지를 발행합니다.
	@MessageMapping("/chat/{roomId}")
	public void sendMessage(@DestinationVariable(value = "roomId") Long roomId, ChatRequest request) {
		messagingTemplate.convertAndSend("/topic/" + roomId, request);
	}
}

📚 StompEventListener

관련 어노테이션/메서드/클래스

  • @EventListener : 애플리케이션 내에서 특정 이벤트가 발생했을 때 이를 감지하고 특정 메서드를 실행하도록 해주는 역할을 한다. 클라이언트가 최초로 연결을 맺게 되면 세션을 생성하고 클라이언트가 연결을 종료하거나 타임아웃이 발생하면 세션이 삭제된다.

토이 프로젝트 시 작성했던 코드 정리

@Slf4j
@Component
public class StompEventListener {

	private final Set<String> sessions = ConcurrentHashMap.newKeySet();  // 동시성 이슈를 고려한 ConcurrentHashMap 사용

	@EventListener
	public void handleConnect(SessionConnectEvent event) {
		String sessionId = event.getMessage().getHeaders().get("simpSessionId").toString();
		sessions.add(sessionId);
		log.info("New WebSocket connection established. Session ID: {}. Total sessions: {}", sessionId, sessions.size());
	}

	@EventListener
	public void handleDisconnect(SessionDisconnectEvent event) {
		String sessionId = event.getSessionId();
		sessions.remove(sessionId);
		log.info("WebSocket connection closed. Session ID: {}. Total sessions: {}", sessionId, sessions.size());
	}
}

📚 흐름도

스크린샷 2025-10-10 오후 11 15 34
  • 클라이언트에서 지정 경로로 메시지를 발행하면 서버와 직접 통신하는 것이 아니라 broker에 의해서 채널에 메시지가 전달된다.
  • 이 때, 특정 채널을 구독하고 있는 클라이언트에게 실시간으로 메시지가 전달된다.
  • SimpleBroker란, 메모리 기반 브로커로서 경로에 따라 메시지를 분배하고 클라이언트에게 메시지를 전달하는 역할을 수행한다. 내장 브로커일 경우 Spring에서 제공하는 메모리 기반 브로커를 사용하게 되고 외장 브로커일 경우 대표적으로 Kafka와 Redis가 쓰일 수 있다.
⚠️ **GitHub.com Fallback** ⚠️