STOMP 메시지를 외부 브로커로 넘겨 인스턴스 여럿이 같은 구독 정보를 공유하게 하는 구성.
무엇이 어디로 가는가
-
/app 으로 온 것 — 애플리케이션이 직접 처리 (@MessageMapping)
-
/topic · /queue — 브로커가 팬아웃
-
흐름이 두 갈래로 갈라진다는 점이 핵심이다
-
브로커 장애 시 '구독 전달만' 멈춘다. 컨트롤러는 멀쩡하다
-
헬스체크가 통과하는 채로 기능이 반쪽만 도는 상태가 만들어진다
사용자별 전송이 그냥은 안 되는 이유
convertAndSendToUser(userId, "/queue/alerts", msg)
어느 사용자가 어느 노드에 붙었는지는 각 노드의 SimpUserRegistry 만 알고 있다
- 다른 노드에 붙은 사용자에게는 닿지 않는다
해법
- ① setUserDestinationBroadcast · setUserRegistryBroadcast 로
- 노드 간 레지스트리·목적지 정보를 브로드캐스트한다
- ② 애초에 사용자별 목적지를 쓰지 않고,
- 구독 키를 도메인 식별자로 설계한다 (더 단순하다)
배포가 곧 전면 재연결이다
WebSocket 연결은 상태를 갖는다 파드를 교체하면 그 파드의 연결이 '전부 동시에' 끊긴다
- 클라이언트가 즉시 재시도하면 새 파드가 뜨자마자 재연결 폭주로 넘어간다
대응
- 지수 백오프 + 지터 (필수)
- 롤링 업데이트 시 종료 유예를 충분히 주고 순차 교체
- 재연결 후 놓친 메시지를 커서로 따라잡는 경로를 둔다
- (연결이 살아 있는 동안만 전달되는 구조라면 공백이 그대로 유실이다)
SockJS 폴백의 대가
WebSocket 이 막힌 환경을 위해 SSE·롱 폴링으로 내려간다
내려가는 순간
- 연결·헤더 비용이 원래 이야기로 되돌아간다
- 서버 자원 소모가 커진다 (특히 롱 폴링)
- 지연이 늘고 동작 차이가 생겨 재현이 어려워진다
요즘 환경에서 WebSocket 차단은 드물다 → 필요할 때만 켠다
브로커 없이 푸는 선택지
스프링에 묶이지 않는 구성으로는 Redis Pub/Sub · Kafka · NATS 를 백플레인으로 두고, 각 노드가 자기 연결에만 팬아웃하는 방식도 흔하다
- STOMP 릴레이 — 프로토콜 수준에서 해결. 코드가 거의 안 바뀐다
- 백플레인 직접 — 구현은 늘지만 브로커 선택·메시지 형식이 자유롭다
둘 다 목표는 같다 — 어느 노드로 붙어도 같은 메시지를 받게 하는 것
릴레이가 끊겼는지 어떻게 아나
스프링은 릴레이 연결을 자동으로 재시도하지만, 그 사이의 메시지는 사라진다
관측 지점
- BrokerAvailabilityEvent — 릴레이 가용성이 바뀔 때 발행되는 이벤트
- 이것을 받아 지표로 올리고, 불가 상태면 기능을 축소하거나 알린다
- 브로커 쪽 연결 수 · 큐 깊이
- clientOutboundChannel 의 큐 적체
헬스체크에 이 신호를 넣지 않으면 '파드는 Ready 인데 알림이 안 가는' 상태가 그대로 유지된다
목적지 설계가 곧 확장성 설계다
| 나쁜 예 | /topic/all | 전원이 구독 → 한 사람의 이벤트가 전원에게 팬아웃 |
|---|---|---|
| 좋은 예 | /topic/room.{id} | 관심 있는 사람만 구독 |
브로커의 부하는 '구독자 수 × 메시지 수' 로 늘어난다 목적지를 잘게 쪼갤수록 팬아웃 폭이 줄어든다
RabbitMQ STOMP 는 목적지를 exchange/queue 로 매핑하므로 목적지 카디널리티가 지나치게 높으면 브로커 쪽 객체 수가 폭증한다
- 방 단위는 괜찮고, 사용자 단위 토픽을 수십만 개 만드는 설계는 피한다
메시지 유실을 어디서 막을 것인가
계층별로 보장 수준이 다르다
- 브라우저 ↔ 서버 — 연결이 끊긴 사이의 메시지는 사라진다 (보장 없음)
- 서버 ↔ 브로커 — 브로커 설정에 따라 지속성·확인 응답을 줄 수 있다
- 애플리케이션 — DB 에 이벤트를 적고, 재연결 시 커서로 따라잡는다
"실시간 푸시" 와 "전달 보장" 은 다른 문제다 중요한 알림은 푸시와 별개로 조회 API 를 반드시 둔다
면접 함정
- ❌ "스티키 세션을 켜면 해결된다" → 붙는 곳만 정해진다. 다른 노드의 구독자에게는 여전히 안 간다.
- ❌ "브로커를 붙였으니 유실이 없다" → 연결이 끊긴 사이의 메시지는 그대로 사라진다. 따라잡기 경로가 따로 필요하다.