WebSocket 위에 얹는 간단한 텍스트 기반 메시징 프로토콜(서브프로토콜).
왜 필요한가 — WebSocket에는 개념이 없다
WebSocket은 바이트를 주고받는 통로일 뿐이다. 규격에 있는 건 프레임과 텍스트/바이너리 구분뿐이고, 그 안에 무엇을 담을지는 정해져 있지 않다.
채팅방이 여러 개인 서비스
이 메시지는 어느 방 것인가? → 직접 정의해야 한다
누가 어느 방을 구독 중인가? → 직접 관리해야 한다
구독 해제는 어떻게 표현하나? → 직접 정의해야 한다
결국 각 팀이 {"type":"subscribe","room":"..."} 같은 규약을 매번 새로 만들게 된다. STOMP는 그 규약을 표준화한 것이다.
프레임 구조 — HTTP를 닮았다
SEND
destination:/app/chat
content-type:application/json
{"text":"안녕하세요"}
^@ ← NULL 바이트로 프레임 종료
명령 + 헤더들 + 빈 줄 + 본문이라는 모양이 HTTP와 같아서 읽기 쉽다. 주요 명령은 이렇다.
| 명령 | 뜻 |
|---|---|
CONNECT / CONNECTED | 세션 시작 |
SUBSCRIBE / UNSUBSCRIBE | 목적지 구독·해제 |
SEND | 목적지로 메시지 발행 |
MESSAGE | 서버가 구독자에게 전달 |
ACK / NACK | 수신 확인·거부 |
목적지 규칙이 핵심이다
- /topic/... — 브로드캐스트 — 구독자 전원에게 (채팅방·공지)
- /queue/... — 1:1 — 특정 사용자에게 (개인 알림)
- /app/... — 서버 측 처리 로직으로 라우팅 (메시지를 가공해 다시 발행)
이 관례 덕분에 "어디로 보내고 어디를 구독한다" 가 문자열 하나로 표현되고, 구독 관리를 프레임워크나 브로커에 맡길 수 있다.
언제 쓰고 언제 안 쓰나
구독 단위가 여럿 (채팅방·종목별 시세·문서별 협업)
- STOMP 가 수고를 덜어 준다
연결 하나에 단순한 메시지 한 종류 (진행률·알림 스트림)
- raw WebSocket 이나 SSE 로 충분. STOMP는 오버헤드
스케일아웃에서 주의
STOMP를 쓰더라도 연결이 인스턴스에 묶인다는 사실은 그대로다. 내장 메모리 브로커만 쓰면 한 인스턴스에 붙은 구독자에게만 메시지가 닿는다. 여러 대로 늘리려면 외부 브로커를 붙여 모든 인스턴스가 같은 브로커를 공유해야 한다.
채널과 인터셉터 — 인가를 어디에 거는가
- 핸드셰이크(HTTP) — 필터 체인을 탄다 → 여기서 인증한다
- STOMP 프레임 — 필터를 거치지 않는다 → ChannelInterceptor 로 검사한다
특히 SUBSCRIBE 를 검사하지 않으면 /topic/room.{id} 처럼 목적지에 식별자가 들어갈 때 남의 방을 구독하는 구멍이 그대로 열린다
면접에서 "WebSocket 인증은 어떻게 하나요" 를 물으면 이 두 단계를 나눠 답하는 것이 정답에 가깝다
여러 인스턴스로 늘릴 때 무엇이 깨지나
-
내장 SimpleBroker — 구독 정보가 그 인스턴스 메모리에 있다
- 파드를 2개로 늘린 순간 '메시지가 절반만 온다'
-
외부 브로커 릴레이 — 모든 노드가 같은 브로커를 공유해 팬아웃이 전 노드에 닿는다
- RabbitMQ 는 STOMP 플러그인이 필요하다
-
사용자별 전송 — convertAndSendToUser 는 노드마다 다른 레지스트리를 본다
- 레지스트리·목적지 브로드캐스트를 켜거나,
- 구독 키를 도메인 식별자로 바꾼다
- 레지스트리·목적지 브로드캐스트를 켜거나,
배포와 재연결
연결이 상태를 갖기 때문에 배포는 곧 전면 재연결이다
- 클라이언트에 지수 백오프 + 지터가 없으면 재연결 폭주로 새 파드가 무너진다
- 끊긴 사이의 메시지는 사라진다 → 재연결 후 커서로 따라잡는 경로가 필요하다
- 하트비트를 켜지 않으면 중간 장비가 유휴 연결을 조용히 끊는다
"실시간 푸시" 와 "전달 보장" 은 다른 문제다 — 중요한 알림은 푸시와 별개로 조회 API 를 둔다
언제 쓰지 않나
-
서버→클라이언트 단방향이면 SSE 가 더 싸고 단순하다
-
메시지 종류가 한두 개면 raw WebSocket 으로 충분하다
-
이미 자체 프로토콜이 있으면 얹을 이유가 없다
-
STOMP 가 값을 하는 자리는 '목적지가 여럿이고 구독 관리가 필요할 때' 다
면접 함정
- ❌ "STOMP는 WebSocket의 대체재" → WebSocket 위에 얹는 층이다.
- ❌ "STOMP를 쓰면 스케일아웃이 해결된다" → 구독 관리가 편해질 뿐, 인스턴스 간 전달은 여전히 외부 브로커의 몫이다.