실시간 통신 SSE — 원리에서 1억 연결 설계까지

서버가 클라이언트에게 실시간으로 데이터를 밀어주려면 어떻게 해야 할까. WebSocket이 먼저 떠오르지만, 알림·피드·주가처럼 서버→클라이언트 단방향이면 SSE(Server-Sent Events)가 더 단순하고 HTTP 인프라와 잘 맞는다.

SSE를 제대로 이해하려면 두 스케일을 봐야 한다. 연결 하나가 TCP/HTTP 레벨에서 어떻게 동작하나, 그리고 그 연결이 1억 개로 늘어나면 무엇이 무너지나. 작게 이해한 원리가 크게 설계할 때의 제약을 결정한다.


① 연결 하나 — 원리

HTTP는 요청-응답인데 SSE는 응답을 끝내지 않고 계속 흘려보낸다. 이게 TCP 레벨에서 어떻게 가능한지, text/event-stream 포맷이 왜 그렇게 생겼는지, Nagle 알고리즘이 왜 SSE를 방해하는지, Spring의 SseEmitter가 Tomcat 스레드를 어떻게 쓰는지 — 한 연결의 해부가 모든 것의 출발이다.

SSE 동작 원리 — HTTP, TCP/IP 레이어까지

② 연결 1억 개 — 아키텍처

연결 하나는 스레드와 메모리를 점유한다. 스레드-per-연결 모델은 수만 개에서 무너진다. 그래서 이벤트 루프 모델로 바꾸고, 서버를 여러 대로 늘리면 “어느 서버에 누가 붙어 있나”라는 새 문제가 생긴다. 이걸 Redis Pub/Sub으로 팬아웃하고, Redis가 병목이 되면 또 쪼개고, 좀비 연결을 정리한다. 한 연결의 비용이 곱하기 1억이 되는 순간의 설계.

SSE 연결 1억건 — 현실적인 아키텍처 설계

원리가 설계를 결정한다

①에서 본 “한 연결이 스레드 하나를 잡는다”는 사실이 ②에서 “스레드-per-연결은 안 된다”는 결론으로 직결된다. Nagle을 꺼야 한다는 원리가 대규모에서 지연 튜닝의 근거가 되고, SseEmitter의 동작 방식이 서버당 수용 가능한 연결 수를 정한다. 작은 이해 없이 큰 설계를 하면 숫자가 안 맞는다.

그래서 언제 SSE인가 — WebSocket·폴링과 비교

도입부에서 “단방향이면 SSE”라고 했는데, 그 선택 기준을 더 또렷이.

  • vs WebSocket: WebSocket은 양방향·바이너리·낮은 지연이라 채팅·협업 편집·게임처럼 클라이언트→서버 입력이 잦으면 맞다. SSE는 서버→클라 단방향이고 순수 HTTP라 프록시·인증·자동 재연결(Last-Event-ID)을 공짜로 얻는다. 알림·피드·진행률·주가처럼 푸시만이면 SSE가 단순하다.
  • vs Long Polling: 롱폴링은 이벤트마다 연결을 다시 맺는 오버헤드가 있다. SSE는 연결을 유지한 채 스트림을 흘린다. 대부분 SSE가 효율적이고, 폴링은 이벤트 빈도가 매우 낮거나 구형 프록시 호환이 필요할 때만.
  • vs gRPC streaming: 브라우저가 아니라 내부 서비스 간 스트리밍이면 gRPC server streaming이 낫다. 브라우저 대상이면 EventSource를 기본 지원하는 SSE.

안 맞는 경우: 양방향 통신이 잦거나, 바이너리 대용량 스트림이거나, HTTP/1.1에서 도메인당 6연결 제한에 걸리거나(HTTP/2로 풀어야 함), 클라이언트가 Authorization 헤더를 못 붙이는 제약이 문제면 SSE 대신 다른 선택을 본다.

면접에서 이 그림이 유용한 이유

“실시간 알림을 어떻게 구현하겠나”를 받으면 두 스케일로 답한다. “단방향이라 WebSocket 대신 SSE. 한 연결은 HTTP 응답을 안 끊고 event-stream을 흘리는데, 연결마다 자원을 쓰니 대규모면 이벤트 루프 + 여러 서버 + Redis Pub/Sub 팬아웃 + 좀비 연결 정리가 필요하다.” 원리에서 제약을, 제약에서 아키텍처를 끌어내는 흐름이 자연스럽다.