
목차
- 개요
- WebSocket과 STOMP의 동작 원리
- Spring에서 WebSocket 환경 구성하기
- 실시간 채팅 서버 핵심 구현
- 성능 특성과 대안 기술 비교
- 운영 환경 적용 시 고려사항
- 맺음말
개요
문제 배경
HTTP는 요청-응답(Request-Response) 모델로 설계된 프로토콜입니다. 클라이언트가 먼저 요청을 보내야만 서버가 응답할 수 있다는 구조적 제약은, 서버가 먼저 데이터를 푸시해야 하는 실시간 애플리케이션에서 심각한 한계로 작용합니다. Spring WebSocket과 STOMP를 조합하면 이 제약을 넘어, 서버와 클라이언트가 대등하게 메시지를 주고받는 양방향 통신 채널을 구축할 수 있습니다. 이 글에서는 WebSocket과 STOMP의 동작 원리부터 Spring 기반의 실제 채팅 서버 구현, 그리고 운영 환경에서 마주치는 실질적인 문제까지 단계별로 살펴봅니다.
실시간 채팅, 주식 호가 피드, 협업 편집기처럼 지속적이고 낮은 지연 시간의 데이터 전달이 필요한 상황에서 HTTP 폴링(Polling)은 오랫동안 임시방편으로 사용되어 왔습니다. 클라이언트가 짧은 주기로 서버에 반복 요청을 보내는 방식은 구현이 단순하다는 장점이 있으나, 변경사항이 없을 때도 불필요한 요청이 발생하고 오버헤드가 누적됩니다. 사용자가 많아질수록 이 낭비는 기하급수적으로 커집니다.
기존 방식의 한계
Long Polling은 서버가 새 데이터가 생길 때까지 응답을 지연하는 방식으로, 단순 폴링보다 효율적입니다. 그러나 응답을 받은 직후 클라이언트는 즉시 새로운 요청을 다시 보내야 하므로, 여전히 매번 HTTP 연결을 새로 맺는 비용이 발생합니다. Server-Sent Events(SSE)는 서버에서 클라이언트 방향으로만 데이터를 스트리밍하는 단방향 기술입니다. 구독 피드나 알림처럼 클라이언트 쪽에서 메시지를 보낼 필요가 없는 시나리오에는 적합하지만, 채팅처럼 양방향 메시지 교환이 필수인 경우에는 별도의 POST 요청을 병행해야 하는 복잡성이 생깁니다. 이러한 한계를 근본적으로 해소하기 위해 RFC 6455로 표준화된 WebSocket 프로토콜이 등장하였고, Spring Framework는 이를 고수준 추상화 레이어인 STOMP와 함께 제공합니다.
WebSocket과 STOMP의 동작 원리
WebSocket 핸드셰이크와 업그레이드
WebSocket은 HTTP를 기반으로 연결을 시작하되, 첫 번째 교환 이후 프로토콜을 전환하는 방식으로 동작합니다. 클라이언트는 표준 HTTP GET 요청에 Upgrade: websocket 헤더를 포함해 서버에 전달합니다. 서버가 이 요청을 수락하면 HTTP 101 Switching Protocols 상태 코드를 반환하고, 이 시점부터 해당 TCP 연결은 더 이상 HTTP가 아닌 WebSocket 프로토콜로 운용됩니다. 이 과정을 핸드셰이크(Handshake)라 부르며, 이후에는 양방향으로 프레임(Frame) 단위 메시지를 자유롭게 교환할 수 있습니다.
핵심은 연결 수립 이후 HTTP 오버헤드가 완전히 사라진다는 점입니다. HTTP 헤더는 요청마다 수백 바이트를 소비하지만, WebSocket 프레임 헤더는 최소 2바이트에 불과합니다. 초당 수백 건의 메시지를 주고받는 실시간 시스템에서 이 차이는 네트워크 대역폭과 서버 CPU 사용률 모두에 유의미한 영향을 미칩니다. 또한 연결이 유지되는 동안 서버는 언제든지 클라이언트에 데이터를 푸시할 수 있으므로, 폴링 방식의 지연과 낭비가 구조적으로 제거됩니다.
STOMP가 해결하는 문제
WebSocket 자체는 단순한 바이트 스트림 프로토콜입니다. 어떤 형식으로 메시지를 주고받을지, 특정 채널을 구독하는 방법, 수신 확인(Acknowledgment)을 어떻게 처리할지에 대한 표준이 없습니다. 이를 직접 구현하면 클라이언트와 서버 양쪽에 중복 코드가 생기고, 구독 관리나 메시지 라우팅 같은 인프라성 관심사가 비즈니스 로직에 침투합니다.
STOMP(Simple Text Oriented Messaging Protocol)는 이 빈자리를 채우는 메시징 서브프로토콜입니다. HTTP와 유사한 프레임 구조(커맨드, 헤더, 바디)를 사용하여 CONNECT, SUBSCRIBE, SEND, UNSUBSCRIBE 같은 시맨틱을 정의합니다. 클라이언트는 특정 destination을 구독하고, 서버는 해당 destination으로 메시지를 브로드캐스트합니다. Spring은 이 STOMP 레이어를 내장 메시지 브로커와 연동하여, 개발자가 메시지 라우팅 로직을 직접 작성하지 않아도 채널 기반의 메시지 분배를 자동으로 처리합니다.
메시지 브로커의 역할
Spring WebSocket 모듈에서 메시지 브로커(Message Broker)는 발행자(Publisher)와 구독자(Subscriber) 사이에서 메시지를 중재하는 핵심 컴포넌트입니다. Spring이 내장 제공하는 SimpleBroker는 인메모리 방식으로 동작하며, 별도 외부 의존성 없이 빠르게 구성할 수 있습니다. 클라이언트가 /topic/chat.room.1 같은 destination을 구독하면, 브로커는 해당 목적지에 대한 구독자 목록을 관리합니다. 이후 누군가 그 destination으로 메시지를 발행하면, 브로커는 구독 중인 모든 클라이언트에게 메시지를 전달합니다.
인메모리 브로커는 단일 서버 환경에서는 충분하지만, 다중 서버 클러스터 환경에서는 한계가 드러납니다. 서버 A에 연결된 사용자와 서버 B에 연결된 사용자 사이에서는 메시지가 전달되지 않기 때문입니다. 이 경우 RabbitMQ나 ActiveMQ 같은 외부 메시지 브로커를 STOMP 릴레이(Relay) 방식으로 연동하면, 브로커가 클러스터 내 모든 서버 인스턴스에 메시지를 전파하는 역할을 맡습니다.
Spring에서 WebSocket 환경 구성하기
의존성 및 초기 설정
Spring Boot 프로젝트에서 WebSocket과 STOMP를 사용하려면 spring-boot-starter-websocket 의존성 하나로 충분합니다. 이 스타터는 spring-websocket, spring-messaging 모듈을 포함하며, 내장 브로커 구성에 필요한 모든 클래스를 제공합니다. 별도의 외부 메시지 브로커 없이 인메모리 방식으로 시작하는 것이 권장됩니다. 복잡성은 필요에 따라 점진적으로 추가하는 것이 유지보수와 운영 안정성 측면에서 유리합니다.
설정의 핵심은 @EnableWebSocketMessageBroker 애노테이션을 붙인 설정 클래스에서 두 가지 메서드를 오버라이드하는 것입니다. configureMessageBroker에서는 내장 브로커의 destination 접두사와 애플리케이션 목적지 접두사를 지정하고, registerStompEndpoints에서는 클라이언트가 WebSocket 연결을 맺을 엔드포인트 URL을 등록합니다. SockJS 폴백 옵션을 함께 활성화하면, WebSocket을 지원하지 않는 구형 브라우저 환경에서도 Long Polling 등의 대체 전송 방식으로 자동 전환됩니다.
아래 설정 예제는 /ws 경로를 WebSocket 엔드포인트로 등록하고, /topic으로 시작하는 destination은 내장 브로커가 처리하며, /app으로 시작하는 destination은 @MessageMapping 컨트롤러 메서드로 라우팅되도록 구성합니다.
// WebSocketConfig.java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 내장 인메모리 브로커가 처리할 destination 접두사
registry.enableSimpleBroker("/topic", "/queue");
// @MessageMapping 컨트롤러로 라우팅될 클라이언트 메시지 접두사
registry.setApplicationDestinationPrefixes("/app");
// 특정 사용자에게 1:1로 메시지를 보낼 때 사용할 접두사
registry.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
// 허용할 CORS 출처 (운영 환경에서는 구체적인 도메인으로 제한)
.setAllowedOriginPatterns("*")
// WebSocket 미지원 환경을 위한 SockJS 폴백 활성화
.withSockJS();
}
}// 서버 기동 시 콘솔 출력 예시
INFO o.s.w.s.s.WebSocketHandlerMapping : Mapped URL path [/ws/**] onto handler...
INFO o.s.w.s.s.s.WebSocketServerSockJsSession : Starting SockJS session [...]이 설정만으로 클라이언트는 ws://localhost:8080/ws 또는 SockJS 폴백 경로로 연결을 맺고 STOMP 세션을 시작할 수 있습니다. /topic과 /queue를 모두 브로커 접두사로 등록한 이유는 용도 구분을 위해서입니다. /topic은 1:N 브로드캐스트, /queue는 1:1 또는 특정 사용자 대상 메시지 전달에 관례적으로 사용합니다.
실시간 채팅 서버 핵심 구현
메시지 모델과 컨트롤러 구조
채팅 메시지를 표현하는 도메인 모델은 단순하게 시작하는 것이 좋습니다. 발신자(sender), 내용(content), 메시지 타입(type), 타임스탬프 정도로 구성하면 대부분의 채팅 요구사항을 수용할 수 있습니다. 메시지 타입은 일반 채팅(CHAT), 입장(JOIN), 퇴장(LEAVE)으로 구분하여, 클라이언트가 시스템 메시지와 사용자 메시지를 다르게 렌더링하도록 돕습니다.
@MessageMapping이 붙은 컨트롤러 메서드는 STOMP SEND 프레임이 지정된 destination으로 도착할 때 호출됩니다. 반환값은 @SendTo에 지정된 destination으로 자동 브로드캐스트됩니다. SimpMessagingTemplate을 직접 주입하면 메서드 반환 외에도 코드 어느 곳에서나 특정 destination으로 메시지를 발행할 수 있어, 이벤트 기반 로직 구현 시 유연성이 높아집니다.
아래 예제는 채팅방 메시지 처리 컨트롤러와 입장/퇴장 이벤트 리스너를 구현합니다. 세션 이벤트 리스너를 통해 WebSocket 연결이 끊어질 때 자동으로 퇴장 메시지를 브로드캐스트하는 로직을 추가하면, 클라이언트 측에서 명시적으로 퇴장을 알리지 않아도 방 참여자 목록을 최신 상태로 유지할 수 있습니다.
// ChatMessage.java — 메시지 도메인 모델
@Getter @Setter
public class ChatMessage {
public enum MessageType { CHAT, JOIN, LEAVE }
private MessageType type;
private String roomId;
private String sender;
private String content;
private Instant timestamp;
}
// ChatController.java — STOMP 메시지 처리
@Controller
@RequiredArgsConstructor
public class ChatController {
private final SimpMessagingTemplate messagingTemplate;
// 클라이언트: SEND destination="/app/chat.sendMessage"
@MessageMapping("/chat.sendMessage")
public void sendMessage(@Payload ChatMessage message,
@Header("simpSessionId") String sessionId) {
message.setTimestamp(Instant.now());
// 같은 roomId를 구독한 모든 클라이언트에게 브로드캐스트
messagingTemplate.convertAndSend(
"/topic/chat." + message.getRoomId(), message);
}
// 채팅방 입장 처리
@MessageMapping("/chat.addUser")
public void addUser(@Payload ChatMessage message,
SimpMessageHeaderAccessor headerAccessor) {
// WebSocket 세션에 사용자 정보 저장 (연결 끊김 감지 시 활용)
headerAccessor.getSessionAttributes()
.put("username", message.getSender());
headerAccessor.getSessionAttributes()
.put("roomId", message.getRoomId());
message.setType(ChatMessage.MessageType.JOIN);
message.setTimestamp(Instant.now());
messagingTemplate.convertAndSend(
"/topic/chat." + message.getRoomId(), message);
}
// WebSocket 세션 종료 이벤트 처리
@EventListener
public void handleWebSocketDisconnectListener(
SessionDisconnectEvent event) {
StompHeaderAccessor accessor =
StompHeaderAccessor.wrap(event.getMessage());
Map<String, Object> attrs = accessor.getSessionAttributes();
if (attrs == null) return;
String username = (String) attrs.get("username");
String roomId = (String) attrs.get("roomId");
if (username != null && roomId != null) {
ChatMessage leaveMessage = new ChatMessage();
leaveMessage.setType(ChatMessage.MessageType.LEAVE);
leaveMessage.setSender(username);
leaveMessage.setContent(username + "님이 퇴장했습니다.");
leaveMessage.setTimestamp(Instant.now());
messagingTemplate.convertAndSend(
"/topic/chat." + roomId, leaveMessage);
}
}
}// 클라이언트 수신 메시지 예시 (JSON)
{
"type": "JOIN",
"roomId": "general",
"sender": "alice",
"content": null,
"timestamp": "2026-03-27T10:15:30.123Z"
}
{
"type": "CHAT",
"roomId": "general",
"sender": "alice",
"content": "안녕하세요!",
"timestamp": "2026-03-27T10:15:45.678Z"
}@EventListener로 SessionDisconnectEvent를 처리하는 부분이 중요합니다. 브라우저 탭을 갑자기 닫거나 네트워크가 끊어지는 상황에서는 클라이언트가 명시적인 퇴장 메시지를 보낼 기회가 없습니다. 이 이벤트 핸들러가 없다면, 다른 참여자 화면에는 해당 사용자가 여전히 접속 중인 것으로 표시될 수 있습니다.
채팅방 접근 제어와 인증 연동
Spring Security와 WebSocket을 통합하는 방법은 두 가지입니다. 첫 번째는 HTTP 핸드셰이크 단계에서 인증을 처리하는 방법으로, HttpSessionHandshakeInterceptor를 사용해 HTTP 세션의 사용자 정보를 WebSocket 세션으로 전달합니다. 두 번째는 STOMP CONNECT 프레임의 헤더에 JWT 토큰을 포함시키고, ChannelInterceptor를 구현해 토큰을 검증한 뒤 SecurityContextHolder에 인증 정보를 설정하는 방법입니다.
토큰 기반 인증이 더 유연합니다. 특히 모바일 앱이나 SPA처럼 쿠키 기반 세션을 사용하지 않는 클라이언트와의 연동에서 이점이 있습니다. ChannelInterceptor의 preSend 메서드에서 StompCommand.CONNECT 명령어를 감지하고, 헤더의 Authorization 토큰을 파싱하여 유효하지 않은 경우 MessageDeliveryException을 던지는 방식으로 비인가 연결을 초기 단계에서 차단할 수 있습니다.
특정 채팅방에 대한 구독 권한은 SubscriptionInterceptor에서 StompCommand.SUBSCRIBE 명령어를 처리하여 제어합니다. destination 경로를 파싱해 채팅방 ID를 추출하고, 현재 인증된 사용자가 해당 채팅방의 참여 권한을 가지고 있는지를 데이터베이스 또는 캐시에서 조회합니다. 권한이 없으면 구독 자체를 거부함으로써, 메시지 수신 레벨에서의 접근 제어가 이루어집니다.
성능 특성과 대안 기술 비교
Spring WebSocket의 성능 특성
Spring WebSocket은 기본적으로 Servlet 컨테이너(Tomcat, Jetty 등)의 WebSocket 지원 위에서 동작합니다. 각 WebSocket 연결은 하나의 TCP 연결을 유지하며, Tomcat 기준으로 NIO 커넥터를 사용할 경우 쓰레드를 연결당 하나씩 할당하지 않고 비동기 I/O로 처리합니다. 따라서 수천 개의 동시 연결도 비교적 적은 쓰레드 풀로 처리할 수 있습니다. 실제 프로젝트에서 측정된 기준으로, 4코어 8GB 인스턴스에서 SimpleBroker를 사용하면 약 5,000~10,000개의 동시 연결을 안정적으로 처리할 수 있는 것으로 알려져 있습니다.
그러나 메시지 처리량은 연결 수보다 더 민감한 지표입니다. 내장 SimpleBroker는 구독자 목록을 인메모리 컬렉션으로 관리하므로, 구독자가 많은 채널에 빈번하게 메시지를 발행하면 브로드캐스트 연산이 병목이 될 수 있습니다. 이 경우 각 클라이언트에 메시지를 순차적으로 전달하는 루프가 지연을 일으킵니다. 이를 완화하려면 Spring이 제공하는 TaskScheduler와 OutboundChannel 쓰레드 풀 크기를 적절히 조정해야 합니다.
SSE, Long Polling, WebSocket 비교
기술 선택은 요구사항에서 출발해야 합니다. SSE는 구현이 단순하고 HTTP/2와 자연스럽게 결합되며, 서버 푸시만 필요한 시나리오(실시간 알림, 대시보드 업데이트)에서는 WebSocket보다 가볍습니다. 프록시 서버나 방화벽 환경에서도 표준 HTTP 위에서 동작하므로 호환성 문제가 적습니다. 반면 클라이언트에서 서버로 데이터를 전송하려면 별도의 HTTP 요청이 필요하다는 점이 채팅 같은 양방향 상호작용에는 걸림돌입니다.
Long Polling은 WebSocket을 지원하지 않는 레거시 환경이나 방화벽이 WebSocket 업그레이드를 차단하는 기업 환경에서 최후의 수단으로 유효합니다. SockJS가 이 폴백을 자동으로 처리하므로, Spring WebSocket 설정에서 .withSockJS()를 활성화해두면 클라이언트 환경에 따라 자동으로 최적 전송 방식을 선택합니다.
WebSocket은 양방향 저지연 통신이 핵심 요구사항일 때 명확한 선택입니다. 채팅, 온라인 게임, 공동 편집, 실시간 경매처럼 서버와 클라이언트가 빈번하게 양방향으로 소통해야 하는 상황에서 다른 기술로는 동일한 사용자 경험을 구현하기 어렵습니다. 연결 수립 시 핸드셰이크 비용이 있지만, 한 번 연결된 이후에는 매우 낮은 오버헤드로 수많은 메시지를 교환할 수 있습니다.
인메모리 브로커 vs. 외부 메시지 브로커
SimpleBroker는 단일 서버 환경에서 탁월합니다. 별도 인프라 없이 즉시 동작하고, 로컬 개발과 테스트가 간편합니다. 그러나 수평 확장(Scale-Out)이 필요하면 외부 브로커 연동이 필수입니다. Spring은 RabbitMQ와의 STOMP 릴레이 연동을 공식 지원합니다. RabbitMQ의 stomp 플러그인을 활성화한 뒤, registry.enableStompBrokerRelay("/topic", "/queue")로 설정을 변경하면 됩니다. 이 경우 Spring 서버는 STOMP 메시지를 RabbitMQ로 중계하고, RabbitMQ가 모든 서버 인스턴스의 구독자에게 메시지를 전달하는 역할을 맡습니다. Redis Pub/Sub을 활용한 커스텀 릴레이 구현도 가능하지만, STOMP 프레임 파싱과 구독 관리를 직접 처리해야 하는 추가 구현 부담이 있습니다.
운영 환경 적용 시 고려사항
흔한 실수와 함정
가장 빈번하게 발생하는 문제 중 하나는 CORS 설정 오류입니다. WebSocket 핸드셰이크는 HTTP 요청이므로 브라우저의 동일 출처 정책(Same-Origin Policy)이 적용됩니다. setAllowedOriginPatterns("*")는 개발 환경에서는 편리하지만, 운영 환경에서는 허용할 출처를 명시적으로 지정해야 합니다. 와일드카드를 그대로 두면 크로스 사이트 WebSocket 하이재킹(CSWSH) 공격에 노출될 위험이 있습니다.
또 다른 흔한 함정은 메시지 크기 제한입니다. Spring WebSocket은 기본적으로 수신 메시지 버퍼 크기에 제한이 있습니다. 파일 전송이나 이미지 첨부 같은 대용량 페이로드를 WebSocket으로 직접 전달하려 하면 BinaryMessage 버퍼 오버플로우가 발생합니다. 이 경우 WebSocketTransportRegistration의 setMaxTextMessageBufferSize, setMaxBinaryMessageBufferSize 설정을 조정하거나, 대용량 파일은 별도의 HTTP 멀티파트 업로드 엔드포인트를 통해 처리하고 WebSocket으로는 업로드 완료 이벤트만 전달하는 패턴을 권장합니다.
세션 클러스터링 문제도 초기에 간과하기 쉽습니다. Spring Security의 HTTP 세션 기반 인증을 WebSocket에 그대로 적용할 경우, 세션이 특정 서버 인스턴스에만 저장되어 있으면 다른 인스턴스로 로드밸런싱된 WebSocket 연결에서 인증 실패가 발생합니다. Redis Session이나 JWT 토큰 기반 인증으로 세션 상태를 서버 외부에 두는 것이 다중 인스턴스 환경에서 안전합니다.
모니터링과 디버깅
운영 환경에서 WebSocket 연결 상태를 모니터링하려면 몇 가지 지표를 지속적으로 수집해야 합니다. 현재 활성 WebSocket 세션 수, 초당 처리 메시지 수(인바운드/아웃바운드), 브로커 채널의 큐 적체 여부가 핵심 지표입니다. Spring Boot Actuator의 /actuator/metrics 엔드포인트를 통해 spring.integration.* 계열 메트릭을 Prometheus나 CloudWatch로 내보낼 수 있습니다.
디버깅 시에는 STOMP 프레임 수준의 로그가 필요한 경우가 많습니다. application.properties에서 logging.level.org.springframework.web.socket=TRACE로 설정하면 핸드셰이크, STOMP 프레임 파싱, 브로커 라우팅 과정을 상세하게 확인할 수 있습니다. 단, 이 설정은 로그 볼륨이 매우 커지므로 운영 환경에서는 특정 연결 문제 조사 시에만 일시적으로 활성화하는 것이 좋습니다.
하트비트(Heartbeat) 설정도 중요한 운영 항목입니다. STOMP 프로토콜은 클라이언트와 서버 간에 주기적으로 빈 라인을 교환하는 하트비트 메커니즘을 정의합니다. 이를 통해 연결이 실제로 살아있는지 감지하고, 좀비 연결(실제로는 끊어졌지만 서버가 인식하지 못한 연결)을 정리할 수 있습니다. configureMessageBroker에서 registry.enableSimpleBroker().setHeartbeatValue(new long[]{10000, 10000})처럼 10초 간격의 하트비트를 활성화하면 연결 상태 감지의 정확도를 높일 수 있습니다.
확장과 마이그레이션 전략
단일 서버에서 클러스터 환경으로 전환할 때는 로드밸런서 설정에 주의가 필요합니다. WebSocket은 긴 수명의 TCP 연결을 유지하므로, 로드밸런서가 Sticky Session(세션 고정)을 지원해야 합니다. AWS ALB의 경우 대상 그룹 설정에서 Stickiness를 활성화하고, Nginx에서는 upstream 블록에 ip_hash 디렉티브를 추가합니다. Sticky Session 없이 연결 도중 로드밸런서가 요청을 다른 서버로 보내면 연결이 강제 종료됩니다.
인메모리 브로커에서 RabbitMQ 릴레이로 마이그레이션할 때는 다운타임 없이 전환하는 것이 가능합니다. 우선 RabbitMQ 클러스터를 별도로 구성하고 기동한 뒤, Spring 설정에서 enableSimpleBroker를 enableStompBrokerRelay로 교체하고 재배포합니다. 클라이언트 입장에서는 STOMP 프로토콜 레벨에서 변경사항이 없으므로, 클라이언트 코드 수정 없이 서버 측 변경만으로 전환이 완료됩니다. 다만 RabbitMQ 연결 설정(호스트, 포트, 자격증명)이 환경 변수 또는 Spring Cloud Config를 통해 외부화되어 있어야 재배포 시 설정 변경이 용이합니다.
맺음말
핵심 요약
Spring WebSocket과 STOMP의 조합은 실시간 양방향 통신을 구현하는 데 있어 Spring 생태계 내에서 가장 완성도 높은 선택입니다. WebSocket이 제공하는 지속 연결과 낮은 프레임 오버헤드는 HTTP 기반 폴링의 구조적 한계를 해소합니다. STOMP는 그 위에 destination 기반의 라우팅, 구독 관리, 메시지 시맨틱을 부여하여 개발자가 비즈니스 로직에 집중할 수 있게 합니다. Spring의 @MessageMapping 컨트롤러, SimpMessagingTemplate, SessionDisconnectEvent 리스너는 이 모든 것을 일관된 프로그래밍 모델로 통합합니다.
적용 판단 기준
이 기술 스택을 선택해야 하는 상황은 명확합니다. 사용자 간의 즉각적인 메시지 교환이 핵심 기능인 채팅 애플리케이션, 여러 참여자가 동시에 같은 데이터를 편집하는 협업 도구, 실시간 입찰이나 주문 변경이 발생하는 경쟁 기반 서비스가 대표적입니다. 반면 단순한 서버 푸시 알림이나 주기적인 데이터 갱신만 필요하다면 SSE가 더 가볍고 인프라 부담도 적습니다. 기술 선택의 기준은 항상 양방향 실시간 통신이 진정으로 필요한지 여부여야 합니다.
단일 인스턴스로 충분한 트래픽이라면 SimpleBroker에서 시작하고, 수평 확장이 필요해진 시점에 RabbitMQ 릴레이로 전환하는 점진적 접근이 현실적입니다. 처음부터 외부 브로커를 도입하면 운영 복잡성과 비용이 증가합니다. 운영 환경에서는 CORS 출처 제한, JWT 기반 인증, Sticky Session 로드밸런서 설정, 하트비트 구성을 반드시 검토해야 합니다.
다음 단계
채팅 서버 구현 이후 심화 주제로는 세 가지를 권장합니다. 첫째, Spring Security와 WebSocket 통합입니다. JWT 기반 ChannelInterceptor 구현과 채팅방 구독 권한 제어를 심화하면 보안 측면에서 운영 수준의 서버를 완성할 수 있습니다. 둘째, 메시지 영속성입니다. 현재 구현은 브로커 메모리 내에서만 메시지를 처리하므로, 서버 재시작 시 메시지가 소실됩니다. MongoDB나 PostgreSQL과 연동하여 메시지 이력을 저장하고, 신규 접속 시 최근 메시지를 불러오는 기능을 추가하면 완성도가 높아집니다. 셋째, RabbitMQ STOMP 릴레이 연동입니다. 다중 인스턴스 환경으로의 확장을 준비한다면 RabbitMQ STOMP 플러그인 공식 문서와 Spring의 StompBrokerRelayMessageHandler 설정을 다음 학습 단계로 삼을 것을 권장합니다.
'프로그래밍 PROGRAMMING > 자바 JAVA AND FRAMEWORKS' 카테고리의 다른 글
| Spring Authorization Server로 OAuth2 인증 서버 구현하기 (2) | 2026.04.23 |
|---|---|
| API Rate Limiting: 토큰 버킷·슬라이딩 윈도우로 트래픽 제어하기 (0) | 2026.04.15 |
| Spring AI로 RAG 파이프라인 구현하기 (0) | 2026.04.01 |
| gRPC Spring Boot 서비스 간 통신 구현하기 (0) | 2026.03.26 |
| Spring Batch 5로 대용량 데이터 처리하기 (0) | 2026.03.24 |