HTTP polling과 long polling — 실시간을 흉내내는 가장 오래된 방법
서버에 새 데이터가 생기면 클라이언트가 곧바로 알게 만들고 싶을 때 가장 먼저 시도된 방법인 HTTP polling과 long polling을, 동작 방식·트레이드오프·지금도 쓰이는 자리 관점에서 정리했다.
HTTP polling과 long polling — 실시간을 흉내내는 가장 오래된 방법
웹에서 "서버에 새 데이터가 생기면 클라이언트가 곧바로 안다"는 것은 생각보다 까다로운 문제다. HTTP는 본질적으로 요청-응답 구조라서, 클라이언트가 묻지 않으면 서버는 아무것도 먼저 보낼 수 없다. SSE, WebSocket, WebRTC 같은 도구가 등장하기 전까지, 실시간성을 흉내내는 방법은 거의 모두 이 한계를 다른 방식으로 우회하는 변형이었다.
이 글은 그 출발점인 HTTP polling과 long polling을 정리한다. 두 기법의 동작과 한계를 모르면, 그 위에 쌓인 SSE와 WebSocket이 왜 그런 모양으로 설계됐는지 이해하기 어렵다.
먼저 결론
| 상황 | 추천 |
|---|---|
| 데이터가 매우 드물게 바뀌고 수 초 지연이 허용됨 | 단순 polling |
| 이벤트를 빠르게 알고 싶지만 인프라는 단순하게 두고 싶음 | long polling |
| 서버 → 클라이언트 실시간 스트림이 필요함 | SSE (다음 글) |
| 양방향 메시지가 필요함 | WebSocket |
polling 계열은 더 이상 "실시간 통신의 기본 선택지"는 아니다. 다만 운영 단순성, 방화벽 우회, 레거시 시스템 호환 같은 이유로 여전히 살아남아 있다. 어디까지가 polling으로 충분한지를 가늠하는 감각이 이 글의 목표다.
가장 단순한 형태 — short polling
short polling(보통 그냥 polling)은 클라이언트가 일정 간격으로 서버에 같은 요청을 반복해서 보내는 방식이다.
async function poll() {
const res = await fetch("/api/notifications");
const data = await res.json();
render(data);
}
setInterval(poll, 3000);3초마다 한 번씩 새 데이터를 묻는다. 서버 입장에서는 그냥 평범한 GET 요청이라 특별한 처리가 필요 없다.
흐름은 다음과 같다.
Client Server
│ │
│── GET /notifications ──▶│
│◀──── 200 (data) ────────│
│ │
│ (3초 대기) │
│ │
│── GET /notifications ──▶│
│◀──── 200 (data) ────────│
│ │
│ (3초 대기) │
│ ... │이 단순함이 short polling의 장점이자 약점이다.
장점
- HTTP 그대로다. 별도 프로토콜이나 인프라가 필요 없다.
- 서버는 stateless하게 유지할 수 있다. 어떤 요청이든 그 시점의 상태만 응답하면 된다.
- 캐싱, 로드밸런싱, CDN 같은 HTTP 생태계의 도구를 그대로 쓸 수 있다.
- 방화벽이나 프록시가 HTTP만 허용해도 동작한다.
약점
- 새 데이터가 없어도 매번 요청을 보낸다. 대부분의 응답이 빈 결과다.
- 지연이 폴링 간격에 묶인다. 3초 간격이면 평균 1.5초의 지연이 생긴다.
- 간격을 줄이면 트래픽이 선형으로 늘어난다. 1초 간격이면 사용자 1만 명당 초당 1만 요청이다.
- HTTP 헤더는 보통 수백 바이트 이상이다. 응답 본문이 짧을수록 헤더 오버헤드 비율이 커진다.
폴링 간격의 트레이드오프
폴링 간격은 항상 두 비용 사이의 타협이다.
| 짧은 간격 | 긴 간격 |
|---|---|
| 지연이 작다 | 지연이 크다 |
| 트래픽이 많다 | 트래픽이 적다 |
| 서버 부하가 크다 | 서버 부하가 작다 |
| 헤더 오버헤드 비율 ↑ | 헤더 오버헤드 비율 ↓ |
실무에서는 보통 다음 둘 중 하나로 푼다.
- 지수 백오프(exponential backoff): 변경이 없으면 폴링 간격을 점점 늘리고, 변경이 생기면 다시 짧게 줄인다. 트래픽을 줄이되 활발한 시점에는 지연을 작게 유지한다.
- 사용자 활동 기반 조정: 탭이 포커스를 잃으면 폴링을 늦추거나 멈춘다.
document.visibilityState로 감지한다.
let interval = 3000;
document.addEventListener("visibilitychange", () => {
interval = document.hidden ? 60_000 : 3000;
});이런 보정으로도 polling의 본질적 한계, 즉 "응답이 도착해야 다음 요청을 보낼 수 있는" 구조는 바뀌지 않는다.
Long polling — 응답을 미루는 트릭
long polling은 polling의 변형이지만 트레이드오프가 다르다. 서버가 새 데이터가 생길 때까지 응답을 의도적으로 미룬다. 즉, 클라이언트가 보낸 요청 하나가 한참 동안 열린 채로 머문다.
흐름을 비교하면 차이가 분명해진다.
Short polling Long polling
Client Server Client Server
│── req ─▶│ │── req ───▶│
│◀ (빈)──│ │ │ (대기)
│ 3초 │ │ │ (대기)
│── req ─▶│ │ │ 이벤트 발생
│◀ (빈)──│ │◀── 200 ───│
│ 3초 │ │── req ───▶│
│── req ─▶│ │ │ (대기)
│◀ (data)│ ...핵심은 "서버는 데이터가 생길 때까지 응답하지 않는다"는 점이다. 클라이언트는 응답을 받으면 곧바로 새 요청을 또 보낸다. 사실상 항상 하나의 요청이 서버 쪽에서 대기 중인 상태가 유지된다.
이 기법은 2000년대 중반 Comet이라는 이름으로 묶여 불렸고, 구체적인 구현으로는 hanging GET이 가장 많이 쓰였다.
클라이언트 코드
async function longPoll() {
while (true) {
try {
const res = await fetch("/api/long-poll");
if (!res.ok) throw new Error(res.statusText);
const data = await res.json();
render(data);
} catch (err) {
await sleep(backoff());
}
}
}
longPoll();while (true)이 어색해 보일 수 있지만, 의미는 "응답이 오자마자 다시 요청"이다. await가 응답이 도착할 때까지 멈춰 있기 때문에 CPU를 태우지 않는다.
서버 측 부담
문제는 서버 쪽이다. 서버는 데이터가 생길 때까지 응답을 미뤄야 하므로, 연결을 그 시간 동안 잡고 있어야 한다. 사용자가 1만 명이면 1만 개의 HTTP 요청이 동시에 열린 채로 머문다.
이걸 견디려면 서버가 요청 하나당 스레드를 잡는 구조여서는 안 된다. 비동기 I/O나 이벤트 기반 서버(Node.js, Netty, Tomcat NIO, Go 등)가 사실상 필수다. 전통적인 스레드-퍼-요청 모델에서는 동시 연결 수가 워커 수에 묶여 금방 한계를 만난다.
또 한 가지, 프록시나 로드밸런서의 idle timeout을 신경 써야 한다. 보통 30초나 1분이 기본값인데, 그 시간 안에 응답하지 않으면 중간에서 연결이 잘린다. 그래서 long polling은 "데이터가 없어도 N초마다 한 번씩은 빈 응답을 돌려준다"는 약속을 같이 두는 경우가 많다.
Long polling의 한계
long polling은 polling보다 지연이 작고 트래픽이 적지만, 본질적인 한계가 남는다.
1. 단방향이다
서버 → 클라이언트 한 방향에만 효과가 있다. 클라이언트가 서버로 메시지를 보내려면 별도의 POST 요청을 또 띄워야 한다. 즉 "양방향 통신"을 흉내내려면 요청 채널과 응답 채널이 둘 다 따로 필요하다. 코드와 디버깅이 모두 복잡해진다.
2. 한 번에 한 메시지
요청 하나는 응답 하나로 끝난다. 응답이 도착하면 클라이언트가 다시 새 요청을 보내야 하는데, 그 짧은 틈에 다음 이벤트가 발생하면 그 이벤트는 새 요청이 서버에 도착해야만 전달된다. 이벤트가 빠르게 연속되는 환경에서는 이 틈이 누적된다.
3. 헤더 오버헤드는 그대로
응답이 한 번 도착할 때마다 새 요청이 또 나간다. HTTP 헤더(쿠키, 인증 토큰, User-Agent 등)가 매번 전송된다. 짧은 메시지를 자주 주고받는 환경에서는 이 오버헤드가 무시할 수 없다.
4. 연결 자원 점유
서버에서 연결을 오래 잡고 있는 만큼, 동시 연결 수 한계에 일찍 부딪힌다. 클라이언트 쪽도 HTTP/1.1에서는 같은 도메인에 동시 연결이 6개로 제한되기 때문에, long polling이 그중 하나를 계속 점유한다. 다른 요청들이 그 자리를 못 쓴다.
HTTP/2 이후로는 한 TCP 연결 위에서 여러 스트림이 동시에 다중화되므로 이 제약이 완화된다. 다만 long polling이 한 스트림을 길게 점유한다는 사실 자체는 변하지 않는다.
그래도 지금 살아남은 이유
대부분의 신규 시스템에서는 SSE나 WebSocket이 더 자연스러운 선택이지만, polling 계열은 여전히 자기 자리를 가지고 있다.
운영 단순성
polling은 결국 그냥 HTTP다. 인프라, 모니터링, 로드밸런서, 인증 미들웨어가 평소 쓰던 것 그대로 동작한다. WebSocket 같은 별도 프로토콜을 도입하면 모니터링 도구, 메트릭, 디버깅 흐름을 다시 정비해야 한다. 작은 팀에서 이벤트가 분당 몇 건 수준인 기능이라면, polling으로 운영을 단순하게 두는 편이 합리적일 때가 많다.
호환성
오래된 프록시, 회사 내부 네트워크, 일부 모바일 환경에서는 WebSocket 핸드셰이크가 막히는 경우가 있다. 이런 환경의 fallback으로 long polling이 쓰인다. Socket.IO처럼 "WebSocket 우선, 안 되면 long polling으로 자동 폴백"하는 라이브러리가 이런 맥락에서 만들어졌다.
매우 드문 이벤트
데이터가 분 단위, 시간 단위로 가끔 바뀌는 시스템이라면 굳이 상시 연결을 유지할 이유가 없다. 가벼운 polling이 자원 효율적이다.
다음 단계로 가는 다리
polling 계열의 한계를 정리하면 자연스럽게 다음 도구로 넘어가게 된다.
| 한계 | 해결책 |
|---|---|
| 매번 새 요청 → 헤더 오버헤드 | 연결 하나를 유지하고 그 위로 메시지를 흘려보내기 |
| 한 번에 한 메시지 | 서버가 임의 시점에 여러 이벤트를 push |
| 단방향이라 양방향이 복잡 | 양방향 채널 |
처음 두 항목을 해결하는 게 Server-Sent Events(SSE)다. HTTP 응답을 끊지 않고 길게 유지하면서 그 안으로 여러 이벤트를 흘려보낸다. 세 번째까지 해결하는 게 WebSocket이고, 그 핸드셰이크는 HTTP의 Upgrade 메커니즘 위에 얹혀 있다.
다음 글에서 SSE의 동작과 EventSource API를 명세에 가까운 시각으로 정리한다.
참고
Related Posts
같이 읽으면 좋은 글
Server-Sent Events — HTTP 위의 단방향 스트림
polling의 한계를 넘어 서버가 클라이언트에 이벤트를 흘려보내는 가장 단순한 방법인 SSE를, 포맷·EventSource API·자동 재연결·HTTP/2와의 궁합·한계 관점에서 정리했다.