웹 애플리케이션에 알림 기능을 추가한다고 가정해보자. 서버에서 새로운 알림이 발생했을 때, 사용자가 페이지를 새로고침하지 않아도 화면에 표시하려면 어떻게 해야 할까?
클라이언트가 주기적으로 서버에 변경 사항을 물어볼 수도 있고, 이벤트가 발생했을 때 서버가 데이터를 전달하도록 통신 경로를 유지할 수도 있다. 대표적인 방식이 폴링(Polling), 롱 폴링(Long Polling), WebSocket, SSE(Server-Sent Events)다. [3][5][9]
언제 요청하는가? 응답을 얼마나 오래 유지하는가? 어느 쪽에서 메시지를 보낼 수 있는가?
이 글에서는 위 세 가지 질문을 중심으로 각 기술을 비교한다. 여기서 ‘실시간’은 이벤트 발생 후 적은 지연으로 정보를 전달한다는 의미다. 특정 기술을 사용한다고 해서 지연이 없어지거나, 정해진 시간 안에 전달이 보장된다는 뜻으로 사용하지는 않는다.
HTTP에서는 일반적으로 클라이언트가 요청을 보내고, 서버가 그 요청에 대한 응답을 반환한다. 클라이언트는 요청에 데이터를 담아 전송하고, 서버도 응답에 데이터를 담을 수 있다. 따라서 HTTP를 단순히 “데이터가 한 방향으로만 흐르는 프로토콜”이라고 설명하면 부정확하다. 핵심은 데이터의 방향보다 요청과 응답의 관계에 있다. [1]
반면 WebSocket은 연결 수립 후 클라이언트와 서버가 상대방의 새 요청을 기다리지 않고 메시지를 보낼 수 있다. [5]
HTTP의 무상태성(Stateless)은 각 요청의 의미를 독립적으로 해석할 수 있다는 뜻이다. 애플리케이션이 사용자 정보를 저장할 수 없다는 의미는 아니다. 사용자 상태 관리는 애플리케이션이 별도의 메커니즘으로 구현한다. [1]
무상태성이 “응답을 보내면 반드시 네트워크 연결을 끊는다”는 뜻도 아니다. HTTP/1.1은 기본적으로 지속 연결을 사용하고, HTTP/2는 하나의 연결에서 여러 요청·응답 스트림을 다중화한다. 새 HTTP 요청이 발생했다고 해서 매번 새 TCP 연결을 만드는 것은 아니다. [2][4]
HTTP 요청·응답의 시작과 종료는 네트워크 연결의 시작과 종료와 다르다.
폴링은 클라이언트가 일정 주기로 HTTP 요청을 보내 새로운 데이터나 변경 사항을 확인하는 방식이다. 서버는 각 요청에 대해 현재 확인할 수 있는 결과를 응답한다. [3]

그림 1. 폴링은 정해진 주기로 변경 사항을 확인한다. 요청·응답이 반복된다는 뜻이며, 매번 TCP 연결을 새로 만든다는 뜻은 아니다. [2][3]
예를 들어 5초마다 알림을 조회한다고 하자. 0초와 5초에는 새 알림이 없고, 7초에 알림이 발생했다면 클라이언트는 10초의 다음 조회에서 이를 확인한다. 이 예시에서는 폴링 주기 때문에 약 3초의 추가 대기시간이 생긴다.
변경 사항이 없어도 요청과 응답을 반복하므로, 폴링 주기를 짧게 할수록 변경을 빨리 확인하는 대신 요청 처리량이 커질 수 있다. [3]
클라이언트 수: N
클라이언트별 폴링 주기: T초
평균 요청량 ≈ N / T건/초
평균 추가 대기시간 ≈ T / 2초
평균 대기시간은 이벤트 발생 시점이 폴링 주기 안에서 균등하다고 가정한 계산이다. 네트워크 지연, 서버 처리시간, 실패와 재시도는 제외한다. 예를 들어 클라이언트 1만 개가 5초마다 요청하면 평균 약 2,000건/초, 1초마다 요청하면 약 1만 건/초가 된다.
폴링에는 실제 데이터 외에 HTTP 헤더와 요청 처리에 필요한 부가 비용이 반복된다. 전달할 데이터가 작을수록 헤더가 전체 전송량에서 차지하는 비중이 커질 수 있다. HTTP/2의 헤더 압축은 전송 비용을 줄일 수 있지만, 반복 요청을 처리하는 구조 자체를 없애지는 않는다. [3][4]
설계 예시: 수십 초 단위의 갱신으로 충분한 작업 상태 조회라면 폴링을 먼저 검토할 수 있다. ‘가능한 한 짧은 주기’보다 서비스가 허용하는 갱신 지연을 먼저 정하고, 그에 따른 요청량을 계산하는 접근이 좋다.
롱 폴링은 전달할 데이터가 없다면 서버가 응답을 즉시 완료하지 않고 기다리는 방식이다. 이벤트가 발생하거나 타임아웃에 도달하면 응답하고, 클라이언트는 응답을 받은 뒤 다시 요청한다. 이미 전달할 데이터가 있다면 바로 응답할 수도 있다. [3]

그림 2. 롱 폴링의 이벤트 발생 경로. 대기 단계에서는 응답을 보류하며, 이벤트가 없더라도 타임아웃에 도달하면 응답한 뒤 다시 요청한다. [3]
롱 폴링에서는 응답 완료 후 새 HTTP 요청을 보내지만, 기존 네트워크 연결을 재사용할 수 있다. 따라서 이를 매번 TCP 재연결이나 TLS 핸드셰이크가 발생하는 과정으로 해석하면 안 된다. [2]
이벤트가 드물면 짧은 주기의 폴링보다 빈 응답을 줄일 수 있다. 반대로 이벤트가 자주 발생하면 응답 완료와 후속 요청이 반복되어 처리 비용이 커질 수 있다. [3]
1,000명이 참여한 채팅방에서 메시지 하나를 모두에게 전달한다고 가정해보자. 각 사용자의 대기 요청에 응답하면 후속 요청 1,000개가 비슷한 시점에 들어올 수 있다. 그러나 메시지를 1,000명에게 전달하는 비용 자체는 다른 방식에도 필요하다. 이 예시에서 구분할 비용은 롱 폴링 응답을 완료하고 다음 요청을 처리하는 과정이다.
따라서 ‘소수 사용자에게만 적합하다’고 단정하기보다 이벤트 빈도와 대기 요청 관리 방식을 함께 검토해야 한다. 비동기 요청 처리를 활용하면 응답을 기다리는 내내 요청 처리 스레드를 점유할 필요는 없다. [12]
설계 예시: 일반 HTTP 요청·응답 구조를 유지하면서 빠른 변경 감지가 필요한 경우 롱 폴링을 검토할 수 있다. 응답과 다음 요청 사이의 이벤트 누락을 막아야 한다면 마지막 수신 위치나 이벤트 보관·재조회 방식도 설계에 포함한다.
WebSocket은 연결 수립 후 클라이언트와 서버가 독립적으로 메시지를 보낼 수 있는 프로토콜이다. 텍스트와 바이너리 메시지를 지원하며, 메시지마다 새 HTTP 요청·응답을 만들 필요가 없다. 다만 WebSocket 프레임과 제어 메시지에 필요한 비용은 존재한다. [5]

그림 3. HTTP/1.1 기반 WebSocket 연결 예시. 아래의 메시지 화살표는 WebSocket 프레임이며, 메시지마다 HTTP 요청·응답을 반복하지 않는다. [5]
TCP 연결 수립
↓
TLS 핸드셰이크 — wss 사용 시
↓
HTTP Upgrade 기반 WebSocket 핸드셰이크
↓
WebSocket 프레임 송수신
위 순서는 일반적인 신규 HTTP/1.1 기반 연결을 설명한다. 클라이언트는 Upgrade: websocket, Connection: Upgrade 등을 포함한 요청을 보내고, 서버는 승인 시 101 Switching Protocols로 전환을 알린다. WebSocket 핸드셰이크는 TCP 연결 수립을 대체하는 절차가 아니다. [5]
HTTP/2와 HTTP/3에는 확장 CONNECT를 사용하는 WebSocket 수립 표준도 있다. 이 경우 그림과 같은 HTTP/1.1 Upgrade·101 흐름을 그대로 사용하지 않는다. 실제 사용 가능 여부는 클라이언트·서버·중간 장비의 지원을 확인해야 한다. [6][7]
WebSocket을 사용해도 업무 차원의 수신 확인이나 오류 처리가 자동으로 완성되지는 않는다. 브라우저의 기본 WebSocket API에는 자동 재연결 기능이 없으므로, 재접속과 구독 복원은 별도로 구현한다. 수신 속도가 처리 속도를 초과할 때의 버퍼 관리도 필요하다. [8]
별도 프로세스나 별도 포트의 서버를 반드시 구축해야 하는 것도 아니다. 기존 웹 애플리케이션에 WebSocket 처리 기능을 통합할 수 있으며, Spring도 이러한 구성을 지원한다. [15]
설계 예시: 양측이 빈번하게 메시지를 보내는 채팅, 공동 편집과 같은 상호작용에서는 WebSocket을 우선 검토할 수 있다. 단순히 서비스 이름으로 결정하기보다 실제 메시지 방향과 빈도를 기준으로 판단한다.
SSE는 클라이언트의 최초 HTTP 요청으로 시작한 응답을 종료하지 않고, 서버가 이벤트를 계속 전달하는 방식이다. 브라우저에서는 EventSource API로 구독할 수 있다. 요청이 전혀 없는 것이 아니라, 이벤트마다 새 요청을 보내지 않는 것이다. [9][10]

그림 4. SSE는 한 HTTP 응답 안에서 여러 이벤트를 전달한다. 그림의 재연결 단계는 선택적으로 켜는 기능이라는 뜻이 아니다. EventSource는 일반적인 연결 단절 시 기본적으로 재연결하며, 누락 이벤트 복구는 별도 구현이다. [9]
롱 폴링은 응답을 완료하고 다음 요청을 기다린다. SSE는 같은 응답을 유지하면서 다음 이벤트를 보낸다. 이것이 두 방식의 핵심 차이다. [3][9]
SSE 응답은 Content-Type: text/event-stream을 사용하며, 데이터는 UTF-8 텍스트 형식이다. 아래는 JSON 문자열을 이벤트 데이터로 담은 예시다. [9][10]
id: notification-101
event: notification
data: {"message":"새로운 알림이 도착했습니다."}
id: notification-102
event: notification
data: {"message":"요청한 작업이 완료되었습니다."}
data는 전달할 내용, event는 이벤트 유형, id는 이벤트 식별자를 나타낸다. 이벤트는 빈 줄로 구분하며, retry로 재연결 대기시간을 지정할 수도 있다. JSON은 SSE 자체의 형식이 아니라 data에 담는 표현 방식이다. [10]
요청의 Accept: text/event-stream은 사용할 수 있지만 명세상 반드시 설정해야 하는 항목은 아니다. 정상적인 스트림 응답의 콘텐츠 유형과 구분해야 한다. 또 Transfer-Encoding: chunked는 HTTP/1.1의 전송 프레이밍 방식일 뿐 SSE의 필수 조건이 아니며, HTTP/2에서는 사용하지 않는다. [9][2][4]
SSE 스트림의 애플리케이션 데이터 방향은 서버 → 클라이언트다. 같은 스트림에서 클라이언트 메시지를 서버로 보내는 기능은 제공하지 않지만, 별도 HTTP API와 함께 사용할 수 있다. [10]
사용자 명령·데이터 등록 : 클라이언트 → HTTP API → 서버
알림·작업 진행 상황 수신 : 클라이언트 ← SSE ← 서버
설계 예시: 사용자가 HTTP API로 작업을 시작하고, 진행 상황과 완료 알림은 SSE로 받도록 역할을 나눌 수 있다.
EventSource는 일반적인 연결 단절 시 자동 재연결을 지원하고, 마지막 이벤트 ID가 있으면 재연결 요청에 Last-Event-ID를 전달할 수 있다. 명시적으로 종료하거나 복구 불가능한 오류가 발생한 경우까지 무조건 재시도하는 것은 아니다. [9]
예를 들어 101번까지 받은 뒤 연결이 끊겼고, 그동안 102번부터 105번까지 발생했다고 하자. 서버가 이를 다시 보내려면 이벤트를 보관하고 마지막 수신 위치에 따라 조회·재전송하는 로직이 있어야 한다. 중요한 알림이라면 보관 기간, 중복 처리, 복구 불가 시 전체 상태 재조회 방법도 정해야 한다.
자동 재연결은 통신 경로를 다시 여는 기능이지, 누락 데이터의 복구나 중복 없는 처리를 보장하는 기능은 아니다.
HTTP/1.1에서 흔히 언급하는 ‘6개’는 주요 브라우저의 도메인별 동시 연결 제한을 가리킨다. 여러 탭에서 SSE를 열 때 영향을 받을 수 있지만, 서버가 사용자 여섯 명만 수용한다는 뜻은 아니다. [10]
HTTP/2에는 연결 내 동시 스트림 제한이 있으며 상대가 알리는 설정에 영향을 받는다. 100개는 고정 상한이나 서버 전체 접속자 수 제한이 아니다. [4]
접속 중인 화면에 이벤트를 보내는 것과, 페이지가 열려 있지 않아도 브라우저 알림을 받는 것은 다른 요구사항이다. 후자에는 Push API와 서비스 워커를 사용하는 Web Push를 별도로 검토해야 한다. [11]
Spring MVC는 롱 폴링과 SSE 등에 활용할 수 있는 비동기 요청 처리 API를 제공한다. WebSocket도 기존 웹 애플리케이션에 통합할 수 있다. [12][15]
| 기능 | 관련 API | 역할 |
|---|---|---|
| 롱 폴링 | DeferredResult |
결과가 준비된 시점에 응답 완료 [13] |
| SSE | SseEmitter |
하나의 응답으로 여러 SSE 이벤트 전송 [14] |
| WebSocket | WebSocketHandler 등 |
연결과 메시지 처리 [15] |
DeferredResult는 Spring 3.2부터, SseEmitter는 Spring 4.2부터 제공된다. [13][14]
중요한 것은 비동기 요청 처리와 비블로킹 I/O를 같은 개념으로 보지 않는 것이다. Spring MVC의 비동기 요청 처리는 응답을 열어 둔 채 요청 처리 스레드를 반환할 수 있다. 다만 개별 응답 쓰기는 블로킹 방식이며, 비블로킹 I/O를 사용하는 WebFlux와 차이가 있다. [12]
따라서 연결 하나를 유지한다는 사실만으로 스레드 하나를 계속 점유한다고 판단하면 안 된다. 프레임워크가 API를 제공하더라도 구독자 관리, 이벤트 전달, 타임아웃, 오류 처리와 자원 정리는 애플리케이션에서 설계해야 한다.
서버가 이벤트를 보냈더라도 중간 프록시가 응답 데이터를 모아서 전달하면 클라이언트가 늦게 받을 수 있다. NGINX에서는 SSE 경로의 proxy_buffering이나 X-Accel-Buffering 설정과 유휴 타임아웃을 검토한다. 캐시와 버퍼링은 별개의 문제다. [16]
WebSocket도 프록시의 프로토콜 전환 전달과 연결 타임아웃을 확인해야 한다. [17]
연결이 끊긴 클라이언트들이 동시에 재접속하면 복구 시점에 부하가 집중될 수 있다. WebSocket 재연결에는 초기 대기시간을 분산하고 반복 실패 시 간격을 늘리는 방법 등을 검토한다. [5]
연결은 살아 있지만 클라이언트가 메시지를 충분히 빨리 처리하지 못하는 경우도 있다. 브라우저의 기본 WebSocket API에는 수신 처리량을 자동 조절하는 백프레셔 기능이 없으므로, 버퍼와 처리량에 대한 별도 설계가 필요하다. [8]
업무 설계에서는 서버가 전송을 시도한 상태 → 클라이언트가 수신한 상태 → 애플리케이션이 처리를 완료한 상태를 구분하는 것이 좋다. 중요한 업무 알림이라면 요구되는 신뢰성 수준에 맞춰 수신 확인, 처리 결과 회신, 이력과 재조회 방식을 정한다.
| 비교 항목 | 폴링 | 롱 폴링 | WebSocket | SSE |
|---|---|---|---|---|
| 기본 방식 | 일정 주기로 조회 | 이벤트·타임아웃까지 응답 보류 | 연결된 채널에서 메시지 교환 | HTTP 응답 스트림으로 이벤트 전달 |
| 반복 단위 | 주기마다 요청·응답 | 응답 완료 후 다음 요청 | WebSocket 메시지·프레임 | 동일 응답 안의 여러 이벤트 |
| 서버 이벤트 전달 | 다음 조회에서 확인 | 대기 중인 요청에 응답 | 서버가 메시지 송신 | 서버가 이벤트 송신 |
| 클라이언트 데이터 송신 | HTTP 요청 | HTTP 요청 | 같은 채널 | 별도 HTTP 요청 등 |
| 주요 설계 관심사 | 조회 주기와 요청량 | 대기·후속 요청 처리 | 양방향 메시지와 연결 관리 | 스트리밍과 재연결·복구 |
표는 구조적 차이를 요약한 것이며, 모든 환경에서의 속도나 자원 효율 순위를 의미하지 않는다. [3][5][9]
| 요구사항 | 우선 검토할 방식 | 판단 방향 |
|---|---|---|
| 일정한 갱신 지연을 허용한다. | 폴링 | 갱신 주기와 예상 요청량 계산 |
| 일반 HTTP 요청·응답 구조로 빠르게 변경을 감지한다. | 롱 폴링 | 대기 요청 수와 이벤트 빈도 검토 |
| 접속 중인 화면에 알림·진행 상황을 전달한다. | SSE + HTTP API | 서버 이벤트 수신과 사용자 요청 분리 |
| 양측이 빈번하게 독립적인 메시지를 보낸다. | WebSocket | 양방향 채널과 재접속·복원 설계 |
| 페이지가 열려 있지 않아도 알림이 필요하다. | Web Push | 서비스 워커 기반 푸시 구독·수신 [11] |
위 기준은 앞서 설명한 특성에 따른 설계 출발점이다. 채팅이라도 송신은 HTTP API, 수신은 SSE로 나눌 수 있다. 반대로 이미 WebSocket 채널을 운영한다면 알림에도 활용하는 구성을 검토할 수 있다. 서비스 이름보다 실제 통신 패턴과 운영 환경이 중요하다.
실시간 통신 기술을 선택하는 일은 가장 최신이거나 빠르다고 알려진 기술을 고르는 일이 아니다. 허용 지연, 이벤트 빈도, 메시지 방향, 연결 유지 비용, 장애 복구 요구사항에 맞는 구조를 선택하는 일이다.
폴링은 주기적으로 확인하고, 롱 폴링은 응답을 기다리며, SSE는 하나의 응답을 계속 사용하고, WebSocket은 양방향 메시지 채널을 유지한다. 이 차이를 바탕으로 실제 서비스의 요청량과 연결 수, 지연과 복구 동작을 검증해야 한다. [3][5][9]
핵심은 ‘실시간 기술인가’라는 이름이 아니라, ‘언제 요청하고, 무엇을 유지하며, 누가 메시지를 보내는가’다.
본문의 대괄호 번호는 아래 공식 명세와 문서에 연결된다. 그림은 이 글의 설명을 위해 생성한 개념도이며, 실제 패킷 캡처나 성능 측정 결과가 아니다.