Java 아키텍처·Spring 용어 사전
실시간enableStompBrokerRelay · SimpUserRegistry · SockJS

브로커 릴레이

STOMP 메시지를 외부 브로커로 넘겨 여러 인스턴스가 같은 구독 정보를 공유하게 하는 구성.

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 를 반드시 둔다

면접 함정

  • "스티키 세션을 켜면 해결된다" → 붙는 곳만 정해진다. 다른 노드의 구독자에게는 여전히 안 간다.
  • "브로커를 붙였으니 유실이 없다" → 연결이 끊긴 사이의 메시지는 그대로 사라진다. 따라잡기 경로가 따로 필요하다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 웹 — MVC·REST·검증·예외·WebSocket/STOMP