이전 31단계에서 우리는 순수 WebSocket을 사용해 양방향 통신을 뚫어보았지만, "단톡방 구분이 안 된다", "메시지 포맷이 중구난방이다", "서버가 여러 대면 통신이 끊긴다"는 치명적인 세 가지 장벽에 부딪혔습니다. 실무 카카오톡이나 디스코드 같은 대규모 실시간 서비스는 이 문제를 어떻게 해결할까요? 바로 웹소켓이라는 텅 빈 고속도로 위에, STOMP(Simple Text Oriented Messaging Protocol)라는 엄격한 교통 규칙(신호등과 차선)을 세우고, 여러 서버 간의 메시지 배달을 책임지는 RabbitMQ(메시지 브로커)라는 거대한 우체국을 도입하는 것입니다.
이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "실무 실시간 서비스 아키텍처의 절대 표준!" STOMP 프로토콜의 Pub/Sub 패턴과 외부 메시지 브로커(RabbitMQ) 연동을 통한 분산 채팅 시스템 구축에 대하여 다뤄보겠습니다.

📫 1. STOMP의 마법: Pub/Sub (발행/구독) 패턴
STOMP는 우편 배달 프로토콜입니다. 단순한 텍스트 덩어리가 아니라, COMMAND, header, body를 갖춘 완벽한 편지 봉투 규격을 제공합니다. STOMP를 도입하면 우리는 더 이상 List<Session>을 만들어서 for문으로 메시지를 뿌리는 노가다를 할 필요가 없습니다. 모든 것은 구독(Subscribe)과 발행(Publish)이라는 우아한 개념으로 바뀝니다.

사용자 A가 '1번 방'에 들어오면 서버에 SUBSCRIBE /sub/chat/room/1 이라는 명령을 보냅니다. 이제 사용자 B가 PUBLISH /pub/chat/message (body: 안녕!)을 보내면, 스프링 서버(내장 브로커)가 알아서 1번 방을 구독(Subscribe) 중인 모든 사람을 찾아내어 안녕!이라는 메시지를 뿌려줍니다. 스프링 부트에서는 이를 @MessageMapping 어노테이션 하나로 놀랍도록 쉽게 처리합니다.
⚙️ 2. STOMP 서버 세팅 및 컨트롤러 구현
@Configuration
@EnableWebSocketMessageBroker // 🌟 STOMP 메시지 브로커 활성화
public class StompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 프론트엔드가 최초로 웹소켓 Handshake를 맺을 주소 (ws://localhost:8080/ws-stomp)
registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 1. 서버가 클라이언트에게 메시지를 뿌릴 때(구독) 앞에 붙일 Prefix
registry.enableSimpleBroker("/sub");
// 2. 클라이언트가 서버로 메시지를 보낼 때(발행) 앞에 붙일 Prefix
registry.setApplicationDestinationPrefixes("/pub");
}
}
@RestController
@RequiredArgsConstructor
public class ChatController {
private final SimpMessageSendingOperations messagingTemplate; // 메시지를 원하는 방에 뿌려주는 핵심 마법 지팡이
// 클라이언트가 "/pub/chat/message" 로 메시지를 날리면 이곳으로 들어옵니다!
@MessageMapping("/chat/message")
public void sendMessage(ChatMessage message) {
// 비즈니스 로직 처리 (DB 저장 등...)
// "/sub/chat/room/{roomId}" 를 구독 중인 모든 클라이언트에게 메시지 발사! (브로드캐스팅)
messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
}
}
🐰 3. 인메모리 브로커의 한계와 RabbitMQ 도입
위 코드에서 사용한 enableSimpleBroker는 스프링 부트 메모리(RAM) 내부에서 돌아가는 장난감(?) 브로커입니다. 서버를 2대, 3대로 늘리면(Scale-out), 1번 서버의 내장 브로커와 2번 서버의 내장 브로커는 서로 단절됩니다. 1번 서버의 철수가 백날 떠들어도 2번 서버의 영희는 들을 수 없죠.
이 장벽을 허물기 위해, 1번 서버와 2번 서버 외부에 거대한 공용 우체국(External Message Broker)인 RabbitMQ를 띄웁니다. 스프링 부트의 설정을 enableStompBrokerRelay()로 딱 한 줄 바꾸면, 이제 1번 서버가 메시지를 받으면 무조건 RabbitMQ에게 던집니다. 그럼 RabbitMQ가 1번 서버와 2번 서버 양쪽 모두에 메시지를 쫙 뿌려주어, 영희의 화면에도 철수의 메시지가 실시간으로 나타나게 됩니다. 이것이 초당 수만 개의 메시지를 감당하는 실무 스케일 아웃 채팅 아키텍처의 정석입니다.
🎯 4. 마무리 및 다음 단계
지금까지 웹소켓의 빈 껍데기를 채워주는 STOMP 프로토콜의 우아한 Pub/Sub 메시징 규격과, 여러 대의 서버 환경에서도 메시지 유실 없이 100% 통신을 보장해 주는 외부 브로커 RabbitMQ의 웅장한 분산 연동 아키텍처에 대해 상세히 다루어 보았습니다. 이제 여러분은 천만 명이 접속하는 채팅 시스템도 설계할 수 있습니다.
실시간 서비스는 완벽합니다. 그런데 어느 날, 회사 재무팀에서 "매월 1일 새벽 4시마다 우리 회원의 지난달 결제 내역 1,000만 건을 전부 뒤져서 VIP 등급을 업데이트하고 쿠폰을 발급해 줘"라는 무시무시한 요구사항을 던집니다. 1,000만 건을 일반 for문으로 돌리며 @Transactional을 걸었다간 3초 만에 메모리가 터져(OOM) 뻗어버릴 것입니다. 이런 무식하게 거대한 '대용량 데이터 일괄 처리'를 위해 백엔드 진영은 어떻게 대처할까요? 이어지는 33단계 포스팅에서는 "수천만 건의 데이터를 쪼개고, 씹고, 뱉어내는 백그라운드의 지배자!" 엔터프라이즈 환경의 필수품, Spring Batch(스프링 배치)의 아키텍처와 기본 원리에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] Spring Batch Chunk 지향 처리: 1,000만 건 메모리 최적화의 꽃 (0) | 2026.08.05 |
|---|---|
| [Spring Boot] Spring Batch 입문: 1,000만 건의 데이터를 씹어 삼키다 (0) | 2026.08.05 |
| [Spring Boot] WebSocket 실시간 채팅: 양방향 통신의 서막 (0) | 2026.08.05 |
| [Spring Boot] GraalVM Native Image: 자바의 한계를 찢고 0.05초 만에 부팅하라 (0) | 2026.07.27 |
| [Spring Boot] Spring WebFlux: 한계 돌파, 비동기 논블로킹(Non-Blocking)의 기적 (0) | 2026.07.27 |