WebSocket vs SSE vs Polling: 실시간 통신 방식 어떻게 고를까

“실시간 알림 만들어야 하는데, WebSocket이면 되나요?”

실시간 기능을 붙이려고 할 때 가장 먼저 듣는 질문이 거의 정해져 있다.

“실시간 알림 기능 만들어야 하는데, WebSocket이면 되나요?”

“그냥 1초마다 API 한 번 부르면 되는 거 아닌가요?”

“SSE는 들어봤는데 정확히 어디 쓰는 거예요?”

“채팅에는 무조건 WebSocket이죠?”

답은 늘 똑같다. ”상황에 따라 다르다.” 채팅에 폴링이 의외로 잘 맞는 경우도 있고, 알림 같은 단방향 흐름에는 WebSocket보다 SSE가 더 깔끔할 때도 있다. 한쪽이 항상 옳은 건 아니다.

이 글은 그 갈림길을 정리한다. 새 시리즈 ‘Realtime & Messaging’의 1편이고, 이후 글들에서 WebSocket 실무, 메시지 큐(RabbitMQ·Kafka)까지 이어진다. 이번 글은 그 시리즈의 출발점으로, ”판단 기준”을 먼저 잡는 데 무게를 둔다.

세 방식의 차이는 결국 ‘누가, 얼마나 자주, 어느 방향으로’ 말을 거느냐다.

결론 먼저: 한 장 비교

‘한 줄 요약: 단방향 푸시는 SSE, 양방향 잦은 메시지는 WebSocket, 단순·낮은 빈도는 Polling. 셋 중 무엇 하나가 항상 옳지는 않다.’

방식방향연결적합한 시나리오부적합한 시나리오
Polling (단순)클라이언트 주도 요청-응답짧게 반복데이터가 가끔 바뀜, 단순함 우선초당 갱신 필요, 사용자 수 많음
Long Polling클라이언트 주도 요청-응답, 서버 응답 지연길게 대기 후 재요청WebSocket이 막힌 환경의 폴백일반적 신규 설계의 1순위는 아님
SSE (Server-Sent Events)서버 → 클라이언트 단방향한 번 연결, 길게 유지실시간 알림, 라이브 피드, 대시보드 업데이트클라가 빠르게 메시지를 보내야 하는 양방향
WebSocket양방향 full-duplex한 번 연결, 길게 유지채팅, 실시간 협업, 게임, 거래 호가창단순 알림, 가벼운 일회성 푸시

핵심 메시지 셋:

  • ‘Polling이 항상 나쁜 건 아니다.’ 변화 빈도가 낮고 사용자 수가 많지 않으면 단순함이 자산이다.
  • ‘단방향 푸시’면 ‘SSE가 가장 깔끔한 카드’다. 표준 HTTP 위에서 동작해 인프라 호환성도 좋다.
  • ‘양방향이 본질’이면 ‘WebSocket’. 채팅·협업·거래·게임처럼 양쪽이 동시에 말을 걸어야 하는 자리.

30초 선택 트리

1. ‘데이터가 5초~30초 늦어도 괜찮고 구현이 제일 중요’하다 → ‘Polling’

2. ‘서버가 클라이언트에게 새 소식만 알려주면 된다’ → ‘SSE’

3. ‘클라이언트도 서버에 자주 말하고, 서버도 클라이언트에 자주 말한다’ → ‘WebSocket’

4. ‘WebSocket이 막히는 고객사·사내망까지 지원해야 한다’ → ‘WebSocket + SockJS 폴백’ 또는 ‘Long Polling 폴백’

5. ‘모바일 앱이 꺼져 있어도 알림이 가야 한다’ → SSE/WebSocket이 아니라 ‘FCM·APNs 같은 푸시 알림’

5번을 한 줄 더 풀면, ‘웹 실시간 통신’과 ‘모바일 푸시 알림’은 다른 문제다. 이 글은 앞의 1~4번에 집중한다.


잠깐 — 왜 일반 HTTP로는 부족한가요?

평소 우리가 쓰는 HTTP는 ‘요청-응답’ 모델이다. 클라이언트가 “뭐 줘”라고 물으면, 서버가 “여기 있어”라고 한 번 답한다. TCP 연결 자체는 keep-alive나 HTTP/2 덕분에 재사용될 수 있지만, ‘하나의 HTTP 요청 관점에서는 응답이 끝나면 그 대화가 끝난다’.

문제는 서버 쪽에 새 이벤트가 생겼을 때다. 기본 HTTP 요청-응답 모델에서는 ‘클라이언트가 다시 묻기 전까지 서버가 먼저 새 응답을 보낼 수 없다’. 채팅 새 메시지, 주가 변동, 알림 푸시처럼 ‘서버가 먼저 말을 걸어야’ 하는 시나리오가 어색해지는 이유다.

이 한계를 우회하는 방법이 ‘Polling'(자주 묻기), ‘SSE'(서버가 한 방향으로 흘려보내기), ‘WebSocket'(처음부터 양방향 연결을 깔아두기)이다.


1. Polling — '자주 묻기' 가장 단순한 방법

‘한 줄 요약: 클라이언트가 일정 주기로 서버에 “뭐 새로운 거 있어?”를 반복해서 묻는다. 단순하지만 트래픽 낭비가 크다.’

어떻게 동작하나

[1초마다] 클라 → 서버: "새 알림 있어?"
[1초 뒤] 서버 → 클라: "없음"
[1초 뒤] 클라 → 서버: "새 알림 있어?"
[1초 뒤] 서버 → 클라: "있음, 메시지 3개"
...

3초에 한 번씩, 1초에 한 번씩, 사용자가 많으면 5초·10초로 늘리는 식이다. 구현은 가장 쉽다 — 그냥 setInterval로 fetch 한 번 보내면 된다.

Polling도 요청과 응답이 오가므로 넓은 의미에서는 양방향처럼 보인다. 다만 ‘서버가 마음대로 먼저 말할 수는 없다’. 클라가 먼저 물어야만 서버가 답한다는 점에서 WebSocket의 full-duplex 양방향과는 다른 구조다.

강점

  • ‘구현이 가장 간단하다.’ 별도 라이브러리·프로토콜 학습 없이 일반 REST로 끝난다.
  • ‘인프라 부담이 없다.’ 표준 HTTP 요청이라 방화벽·프록시·로드밸런서 호환성 100%.
  • ‘클라가 살아있는지 자동 확인된다.’ 어쨌든 주기적으로 요청이 오니까.

약점

  • ‘대부분의 요청이 헛수고다.’ 변화가 없을 때 “없음”만 받고 끊는 요청이 90% 넘는다.
  • ‘실시간성이 떨어진다.’ 5초 폴링이면 새 메시지가 평균 2.5초 지연된다.
  • ‘사용자 수가 늘면 트래픽·DB 부하가 비례해서 폭발한다.’ 동시 사용자 1만 명 × 1초 폴링 = 초당 1만 요청.

‘Long Polling’은 살짝 다르다

기본 폴링 변형으로 ‘Long Polling’이 있다. 클라가 요청을 보내면 서버가 ‘변화가 생길 때까지’ 응답을 미루고 있다가, 변화가 생기면 그때 응답을 돌려준다. 클라는 응답을 받자마자 다시 새 요청을 건다.

  • ‘강점’: 변화가 없을 때의 헛 요청이 사라진다. 실시간성도 폴링보다 좋다.
  • ‘약점’: 서버 쪽이 ‘대기 중인 요청’을 많이 들고 있어야 해서 동시 연결 수가 늘면 부담이 커진다. 구현도 일반 폴링보다 까다롭다.
  • ‘자리’: WebSocket이 막힌 환경의 폴백으로 자주 쓰인다(SockJS도 내부적으로 long polling 폴백을 쓴다). 신규 설계의 1순위는 아니다.

언제 어울리나

  • ‘데이터 변경이 가끔 일어나는 자리’: 사내 공지사항, 잡 큐 상태, 가벼운 알림.
  • ‘사용자 수가 많지 않은 내부 도구’: 폴링 트래픽 부담이 작음.
  • ‘단순함이 자산인 MVP’: 빨리 만들고 나중에 SSE/WebSocket으로 옮길 수 있음.

2. SSE (Server-Sent Events) — '서버가 한 방향으로 흘려보내기'

‘한 줄 요약: 클라가 한 번만 연결하면 서버가 그 연결을 통해 메시지를 계속 흘려보낸다. 단방향, HTTP 기반, 표준이라 인프라 호환성이 좋다.’

어떻게 동작하나

[최초] 클라 → 서버: GET /events  (Accept: text/event-stream)
[연결 유지] 서버 → 클라: "data: 새 알림\n\n"
[잠시 뒤] 서버 → 클라: "data: 또 다른 알림\n\n"
[잠시 뒤] 서버 → 클라: "data: 주가 갱신\n\n"
...

연결을 한 번 열고, 서버가 그 위에서 작은 메시지들을 줄줄이 흘려보낸다. 표준 HTTP 위에서 동작하기 때문에 별도 프로토콜 학습이 거의 없다.

강점

  • ‘단방향 푸시에 가장 깔끔하다.’ 서버 → 클라이언트만 흐르는 시나리오라면 군더더기가 없다.
  • ‘표준 HTTP 위에서 돈다.’ 방화벽·프록시·CDN·로드밸런서 호환성이 좋다.
  • ‘자동 재연결이 표준에 들어 있다.’ 연결이 끊기면 브라우저가 알아서 다시 붙는다.
  • ‘구현이 가볍다.’ 브라우저는 EventSource API 한 줄, 서버도 응답 헤더 + chunked 응답으로 끝.

약점

  • ‘클라이언트가 메시지를 보낼 수 없다.’ 클라 → 서버 메시지는 별도 REST 요청으로 보내야 한다.
  • ‘브라우저별 동시 연결 제한이 있다.’ HTTP/1.1 기준 같은 도메인에 동시 6개 같은 한계가 SSE에도 적용. (HTTP/2 이상에서는 완화.)
  • ‘바이너리 데이터에 부적합.’ 텍스트 기반이라 이미지·오디오 스트림에는 안 맞음.

언제 어울리나

  • ‘실시간 알림’: 새 메시지·이벤트·푸시 같은 단방향 통보.
  • ‘라이브 피드’: 뉴스 피드, 주가 갱신, 댓글 스트림.
  • ‘대시보드 업데이트’: 모니터링 화면, 매출 현황, 게임 점수.
  • ‘AI 응답 스트리밍’: ChatGPT처럼 토큰을 한 글자씩 흘리는 자리.

”AI 챗봇 응답 스트리밍’은 사실상 SSE 형식의 대표 사례다. 클라는 질문 한 번 보내고 답을 받기만 하면 되니까 단방향이면 충분하다. 채팅 같은 양방향처럼 보여도 메시지 송신과 응답 스트림은 분리돼 있다.”

다만 여기서 한 가지를 구분해야 한다. 브라우저의 EventSource API는 주로 GET 기반으로 서버 이벤트를 구독하는 용도다. 반면 ‘AI 챗봇 API는 질문을 POST로 보내고, 응답 body를 text/event-stream 형식으로 흘려받는 경우가 많다’. 즉 AI 응답 스트리밍은 ‘SSE 형식의 스트림’이라고 이해하면 좋고, 브라우저에서 꼭 EventSource만 써야 한다는 뜻은 아니다(fetch streams로도 받을 수 있다).


3. WebSocket — '양방향 연결 한 번 깔아두기'

‘한 줄 요약: 한 번 연결을 열면 그 위에서 양쪽이 자유롭게 메시지를 주고받는다. 양방향이 본질인 자리에 무게가 있다.’

어떻게 동작하나

[최초] 클라 → 서버: HTTP Upgrade: websocket  (handshake)
[연결 유지] 클라 ↔ 서버: 메시지 자유 송수신
   클라 → 서버: "방 입장"
   서버 → 클라: "환영합니다, 현재 12명"
   서버 → 클라: "B 사용자 입장"
   클라 → 서버: "메시지 보내기: 안녕하세요"
   서버 → 클라: "C 사용자 입장"
   ...

처음에 HTTP로 핸드셰이크를 하고 ‘WebSocket 프로토콜로 업그레이드’한다. 그 뒤로는 한 연결 위에서 양쪽이 자유롭게 메시지를 주고받는다. HTTP의 요청-응답 모델을 벗어난다.

강점

  • ‘양방향이다.’ 클라·서버 모두 언제든 메시지를 보낼 수 있다.
  • ‘메시지 오버헤드가 작다.’ HTTP 헤더 없이 작은 프레임으로 주고받음.
  • ‘실시간성이 가장 좋다.’ 한 번 연결되면 추가 핸드셰이크 없이 즉시 송수신.
  • ‘바이너리·텍스트 모두 자연스럽다.’ 게임 패킷, 음성, 영상 메타까지.

약점

  • ‘인프라 호환성이 SSE보다 까다롭다.’ 일부 프록시·방화벽이 WebSocket 업그레이드를 차단할 수 있다. 이 때문에 SockJS 같은 폴백 라이브러리가 등장.
  • ‘연결 상태 관리가 필요하다.’ 끊김 감지·재연결·heartbeat·세션 복구가 모두 직접 챙겨야 할 영역.
  • ‘서버 측 비용이 크다.’ 사용자 수만큼 동시 연결을 들고 있어야 하므로 메모리·파일 디스크립터 관리가 중요.
  • ‘구현 복잡도가 가장 높다.’ 메시지 형식, 인증, reconnect, scale-out까지 설계할 거리가 많다.

언제 어울리나

  • ‘채팅·메신저’: 양방향 + 다수 동시 연결.
  • ‘실시간 협업 도구’: 구글 docs, Figma 같은 협업 편집.
  • ‘게임’: 위치·상태 동기화 같은 빠른 양방향.
  • ‘거래 호가창·라이브 거래소’: 클라 주문 + 서버 호가/체결 양방향 흐름.

비교: 한 장 더

‘한 줄 요약: 방향성·연결 비용·인프라 호환·구현 난이도가 트레이드오프 축이다.’

항목PollingSSEWebSocket
방향양방향(요청-응답)단방향(서버→클라)양방향
연결 유지매번 짧게 끊김한 번 열고 길게한 번 열고 길게
실시간성낮음(주기 의존)높음가장 높음
트래픽 효율낮음(헛 요청 多)높음가장 높음
인프라 호환가장 높음높음(표준 HTTP)보통(프록시 따라 차단 가능)
구현 난이도가장 쉬움쉬움가장 어려움
자동 재연결자체로 반복브라우저 표준 지원직접 구현
바이너리가능(Base64 등)부적합자연스러움
서버 비용요청 수에 비례동시 연결 수동시 연결 수 + 상태 관리

”표 한 줄로 정리하면 — Polling은 단순함, SSE는 단방향 푸시의 표준, WebSocket은 양방향이 본질일 때.”


시나리오로 고르기

‘한 줄 요약: 본질이 단방향이냐 양방향이냐, 빈도가 잦냐 가끔이냐만 잡으면 거의 답이 나온다.’

시나리오추천이유
사내 공지·잡 큐 상태Polling변화 빈도 낮고 사용자 적음
새 알림 푸시SSE단방향 + 가끔 발생
라이브 댓글·피드SSE단방향 스트림
모니터링 대시보드SSE단방향, 자동 재연결 표준
AI 응답 스트리밍SSE단방향 토큰 흐름
1:1 채팅WebSocket양방향 + 실시간
그룹 채팅·메신저WebSocket양방향 + 다대다
실시간 협업 편집WebSocket양방향 + 잦은 동기화
게임·거래소WebSocket양방향 + 빠른 응답
WebSocket이 막힌 환경Long Polling 폴백인프라 제약 우회

본질이 ‘AI가 답을 흘려보낸다’처럼 단방향에 가까운데 ‘WebSocket으로 만들어 두는 경우’가 흔한데, 보통 SSE가 더 어울린다. 반대로 ‘채팅을 폴링으로 만드는 경우’는 사용자 수가 늘면 곧 한계가 온다.


Spring 관점으로 보면

‘한 줄 요약: Spring MVC에서는 SSE는 SseEmitter, WebFlux에서는 Flux<ServerSentEvent>. WebSocket은 native WebSocket 또는 STOMP로 풀고, SockJS는 폴백이 필요할 때 붙인다.’

Polling

특별한 게 없다. 일반 @RestController로 만들고 클라가 주기적으로 호출하면 된다.

SSE — SseEmitter

@GetMapping(value = "/notifications", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@RequestParam Long userId) {
    // 0L은 Servlet async timeout을 끄겠다는 의미에 가깝다.
    // 단, 로드밸런서/프록시 idle timeout은 별도라 heartbeat는 여전히 필요.
    SseEmitter emitter = new SseEmitter(0L);
    notificationBroker.register(userId, emitter);
    emitter.onCompletion(() -> notificationBroker.remove(userId, emitter));
    emitter.onTimeout(() -> notificationBroker.remove(userId, emitter));
    emitter.onError(e -> notificationBroker.remove(userId, emitter));
    return emitter;
}

서버 쪽 어딘가에서 emitter.send(...)를 호출하면 클라이언트의 EventSource(또는 fetch stream)로 메시지가 흐른다. ‘WebFlux를 쓰는 환경이면 Flux<ServerSentEvent> 반환 패턴이 더 자연스럽다.’

‘주의할 점은 timeout 두 종류가 다르다는 것이다.’ SseEmitter timeout을 길게 잡거나 0으로 둬도, 중간 로드밸런서·프록시가 일정 시간 데이터가 없다고 판단하면 연결을 끊을 수 있다. 그래서 heartbeat가 별도로 필요하다. onError도 같이 등록해야 클라가 탭을 닫거나 네트워크가 끊겼을 때 registry에서 emitter를 제때 제거할 수 있다.

WebSocket — native 또는 STOMP

Spring에서 WebSocket을 다루는 대표 경로는 두 가지다.

  • ‘단순 native WebSocket’: 메시지를 직접 주고받는 방식. 단방향에 가까운 양쪽 채널이 필요할 때 충분.
  • ‘STOMP over WebSocket’: /topic, /queue, /app 같은 메시지 라우팅을 얹는 방식. 채팅방·구독·브로드캐스트·사용자별 메시지가 필요할 때 편하다.

채팅처럼 구독·브로드캐스트·사용자별 메시지가 본질이라면 STOMP가 잘 맞는다. ‘SockJS는 WebSocket이 막힌 고객사·사내망까지 지원해야 할 때 붙이는 폴백 카드’다. 최신 브라우저 + 통제된 인프라라면 SockJS 없이 native WebSocket 또는 STOMP만으로도 충분할 수 있다.

@Configuration
@EnableWebSocketMessageBroker
public class WsConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // SockJS는 폴백이 필요할 때만 붙인다
        registry.addEndpoint("/ws").withSockJS();
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic", "/queue");
        config.setApplicationDestinationPrefixes("/app");
    }
}

@MessageMapping("/chat.send") 같은 컨트롤러로 메시지를 받고, SimpMessagingTemplate으로 특정 사용자나 토픽에 푸시한다.

‘주의: enableSimpleBroker는 예제·MVP에는 좋지만 클러스터링에는 한계가 있다.’ Spring 문서도 simple broker는 단일 인스턴스용으로 설계됐고 STOMP 명령 일부도 지원하지 않는다고 안내한다. 운영 규모가 커지고 여러 서버 인스턴스에서 브로드캐스트가 필요하면 RabbitMQ·ActiveMQ 같은 ‘외부 STOMP broker relay’로 옮겨야 한다. (다음 글들의 메시지 큐 주제와 자연스럽게 이어진다.)

Reactive

Spring WebFlux를 쓰면 SSE는 Flux<ServerSentEvent> 반환만으로 자연스럽게 풀린다. WebSocket도 WebSocketHandler로 reactive 처리 가능. 단, 이미 MVC 스택이라면 굳이 reactive로 옮길 필요는 없다 — 트래픽 패턴에 따라 결정.


운영에서 자주 부딪히는 함정

‘한 줄 요약: 통신 방식보다 “스케일·세션·끊김” 문제가 더 자주 사고를 만든다.’

  • ‘세션 stickiness’: 같은 클라가 항상 같은 서버 인스턴스에 붙어야 하는 경우(예: SseEmitter 기반 인메모리 브로커). 로드밸런서에 sticky session 설정이 필요하거나, Redis pub/sub 같은 외부 브로커로 인스턴스 간 메시지를 라우팅해야 한다.
  • ‘Idle timeout’: 프록시·로드밸런서가 일정 시간 아무 메시지도 안 흐르면 연결을 끊는다. heartbeat를 주기적으로 보내야 한다. ‘SSE는 : heartbeat\n\n 같은 주석 라인.’ ‘WebSocket은 프로토콜 차원의 ping/pong frame이 있지만, 브라우저 JavaScript WebSocket API에서는 ping frame을 직접 보낼 수 없다.’ 따라서 클라이언트 쪽에서는 보통 (1) 서버·라이브러리의 ping/pong 기능을 쓰거나, (2) 애플리케이션 메시지로 ping/pong을 직접 정의하거나, (3) STOMP heartbeat 설정을 사용한다.
  • ‘프록시 버퍼링(SSE 한정)’: SSE는 서버가 작은 이벤트를 즉시 flush해야 하는데, Nginx 같은 프록시가 응답을 버퍼링하면 클라가 실시간으로 받지 못하고 한꺼번에 받는다. Nginx 환경이라면 X-Accel-Buffering: no, Cache-Control: no-cache, Content-Type: text/event-stream 헤더를 함께 확인해야 한다. 실무에서 진짜 자주 터지는 자리다.
  • ‘재연결 폭주’: 서버 장애 후 한꺼번에 클라들이 재접속을 시도하면 thundering herd가 생긴다. 클라 쪽 재연결 backoff·jitter가 필요.
  • ‘인증·인가 갱신’: 한 번 연결 후 토큰이 만료되면? 끊고 재연결할지, 메시지 단위로 검증할지 설계가 필요하다.
  • ‘대량 fan-out’: 한 메시지를 1만 명에게 보내야 한다면 인스턴스 안 직접 전송보다 ‘Redis pub/sub, Kafka, 별도 메시지 브로커’를 가운데 두는 패턴이 안전하다.
  • ‘관측성’: 동시 연결 수, 메시지 처리 latency, 끊김 빈도, 재연결 빈도를 메트릭으로 만들어 두지 않으면 사고 후 원인 분석이 매우 어렵다.

”메시지 큐(RabbitMQ·Kafka)는 이런 자리에서 자주 등장한다.” 다음 글들에서 이 부분을 풀어간다.


마치며: 시리즈 'Realtime & Messaging' 첫 글로

이 글은 시리즈 ‘Realtime & Messaging’의 1편이다. REST 너머의 통신 패턴을 한 글씩 정리할 예정이다.

요약하면:

  • ‘Polling’은 단순함이 자산. 변화 빈도 낮고 사용자 수 적은 자리에 잘 맞는다.
  • ‘SSE’는 단방향 푸시의 표준 카드. 알림·라이브 피드·AI 응답 스트리밍에 자연스럽다.
  • ‘WebSocket’은 양방향이 본질일 때. 채팅·협업·게임·거래소.
  • ‘신규 설계의 1순위’는 보통 ‘SSE 또는 WebSocket’. Long Polling은 폴백 카드.
  • ‘Spring 환경’이면 SseEmitter, STOMP+SockJS 조합이 표준 경로.
  • 통신 방식보다 ‘idle timeout, sticky session, heartbeat, fan-out, 관측성’ 같은 운영 함정에 더 자주 사고가 난다.

다음 글은 ‘WebSocket 실무: SockJS + WebStomp로 채팅 만들어보기’ 또는 ‘RabbitMQ와 메시지 큐 입문’으로 이어갈 예정이다. 시리즈는 ‘REST → 실시간 통신 → 비동기 메시징 → 이벤트 스트리밍’의 큰 흐름을 따른다.


📦 추가 메모: 손에 들고 시작할 디테일

브라우저 API 한 줄 요약

// Polling
setInterval(() => fetch('/api/notifications'), 5000);

// SSE
const sse = new EventSource('/api/stream');
sse.onmessage = (e) => console.log(e.data);

// WebSocket
const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = (e) => console.log(e.data);
ws.send('hello');

세 가지 다 브라우저 표준 API. 라이브러리 없이 시작 가능.

동시 연결 한도 빠르게 추정하기

  • ‘SSE/WebSocket 동시 연결 수’ = 사용자 수 × 평균 동시 접속 비율.
  • 인스턴스 한 대당 동시 연결 한계는 OS 파일 디스크립터(ulimit -n), JVM 메모리, 라이브러리 버퍼에 의해 결정. 일반적으로 수만 동시 연결까지는 단일 인스턴스로 가능하지만 정확한 수치는 부하 테스트로 측정해야 한다.
  • 동시 연결 수가 한 인스턴스 한계를 넘으면 ‘수평 확장 + Redis pub/sub 또는 외부 메시지 브로커’로 인스턴스 간 fan-out 처리.

Heartbeat 주기 잡기

  • 일반적으로 클라우드 로드밸런서·프록시의 idle timeout이 60~120초 사이.
  • 안전하게 그보다 짧은 주기(예: 30초)로 heartbeat 전송.
  • SSE는 : heartbeat\n\n 같은 주석 라인, WebSocket은 ping/pong 프레임.

인증 패턴

  • ‘WebSocket’: 핸드셰이크 단계에서 쿠키나 query string으로 토큰 전달. 일부 라이브러리는 첫 메시지(STOMP CONNECT 등)에서 인증 처리.
  • ‘SSE’: 표준 HTTP 요청이라 일반 인증 헤더 그대로. 다만 브라우저 EventSource는 헤더 커스터마이징이 제한적이라 쿠키·세션 기반 인증이 자연스럽다. cross-origin이면 withCredentials + 서버 CORS 설정을 같이 챙긴다.

WebSocket 보안 체크

  • ‘운영에서는 ws://가 아니라 wss://를 쓴다.’ 평문 채널은 토큰·메시지 모두 노출.
  • ‘쿠키 기반 인증을 쓴다면 Origin 검사를 반드시 한다.’ 서버가 핸드셰이크의 Origin 헤더를 검증하지 않으면 cross-site 연결이 가능해진다.
  • ‘Query string에 토큰을 넣으면 access log·proxy log에 그대로 남을 수 있다.’ 짧은 수명 토큰을 쓰거나 로그 마스킹·redaction을 적용한다.
  • ‘연결이 한 번 열렸다고 끝이 아니다.’ 토큰 만료, 방·채널 권한, 메시지별 인가를 별도로 설계한다. 장기 연결 중에 권한이 바뀔 수 있다.

한 줄 정리

”양방향이 본질이면 WebSocket, 단방향 푸시면 SSE, 단순하면 Polling. 그리고 진짜 사고는 통신 방식이 아니라 운영(끊김·재연결·fan-out·관측성)에서 난다.”

댓글 남기기