롱 폴링
고친 사람 github-actions[bot]
롱 폴링은 서버에 새 소식이 생기면 클라이언트가 곧바로 받아 보게 해 줍니다. 클라이언트가 먼저 물어 두면 서버는 소식이 생길 때까지 답을 미뤄 둡니다. 서버가 먼저 말을 걸 수 없는 웹에서 알림을 늦지 않게 전하려고 씁니다.
쉽고 빠른 이해
롱 폴링은 「새 소식 있으면 주세요」라는 요청을 서버가 쥐고 있다가 소식이 생기는 순간 답하는 방식입니다. 채팅 화면에서 상대가 보낸 말이 곧바로 뜨게 하는 데 쓸 수 있습니다.
웹에서는 클라이언트가 물어야 서버가 답합니다. 서버가 먼저 말을 걸 방법이 없습니다. 몇 초마다 되물을 수는 있습니다. 그러면 그 간격만큼 소식이 늦습니다. 되물음 대부분은 「없다」로 끝납니다.
어떻게 도나:
- 클라이언트가 새 소식을 달라고 요청합니다
- 서버는 소식이 없으면 답하지 않고 요청을 쥔 채 기다립니다
- 소식이 생기거나 정해진 시간이 지나면 그때 답합니다
- 클라이언트는 답을 받자마자 다음 요청을 보냅니다
대가도 있습니다. 서버는 기다리는 클라이언트 수만큼 요청을 열어 둔 채 들고 있어야 합니다. 소식이 아주 잦으면 소식마다 요청을 새로 보내는 품이 쌓입니다.
상세
이 절은 빵집 비유로 롱 폴링의 모양부터 잡습니다. 그다음 무엇을 풀려고 나왔는지 보고, 요청 하나가 서버에 붙들렸다가 풀려나는 한 바퀴를 따라갑니다. 끝으로 이 방식이 서버에 지우는 짐을 보고 다른 방식과 견줍니다.
카운터 앞에 서서 기다리는 손님
갓 구운 빵을 사려는 손님이 있다고 해 봅시다. 손님은 몇 분마다 카운터에 가서 나왔는지 묻지 않습니다. 카운터 앞에 서서 「나오면 바로 주세요」라고 말해 둡니다. 점원은 빵이 나오는 순간 손님에게 건넵니다.
빵이 한참 안 나오면 점원이 「아직이에요」라며 손님을 돌려보냅니다. 손님은 곧바로 다시 줄을 섭니다. 빵을 받은 손님도 다음 빵이 필요하면 다시 줄을 섭니다.
서버가 먼저 말을 걸 수 없는 문제
HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜)는 클라이언트가 요청을 보내면 서버가 응답하는 한 쌍으로 돕니다. 응답은 언제나 앞선 요청에 대한 답입니다. 그래서 서버는 보낼 소식이 생겨도 먼저 보낼 길이 없습니다.
채팅·알림·주문 상태처럼 소식이 서버 쪽에서 먼저 생기는 화면이 있습니다. 서버가 못 부르면 클라이언트가 정해진 간격으로 되물을 수밖에 없습니다. 이 되묻기를 폴링이라고 합니다.
폴링에는 대가가 둘 있습니다. 하나는 늦음입니다. 소식이 생긴 순간부터 다음 물음이 올 때까지, 길게는 한 간격을 기다립니다.
다른 하나는 헛걸음입니다. 소식이 드물면 물음 대부분이 「없다」를 듣고 끝납니다. 간격을 좁히면 늦음은 줄어듭니다. 대신 헛걸음이 그만큼 늘어납니다.
롱 폴링은 이 둘을 함께 줄입니다. 서버가 「없다」를 바로 돌려주지 않고 소식이 생길 때까지 답을 미룹니다. 소식이 생긴 순간에 답이 나가므로 늦음이 거의 없어집니다. 기다리는 동안에는 요청이 오가지 않으므로 헛걸음도 줄어듭니다.
요청을 붙들어 두는 한 바퀴
롱 폴링의 본체는 요청 하나가 서버에 붙들렸다가 풀려나는 순서입니다. 소식이 생겨 풀려나는 경우와 기다릴 시간이 다 되어 풀려나는 경우를 한 그림에 놓고 봅니다.
첫 단계에서 서버는 요청에 곧바로 답하지 않습니다. 보낼 소식이 없으면 그 요청을 열어 둔 채 쥐고 있습니다.
sequenceDiagram
participant 클라이언트
participant 서버
클라이언트->>서버: 새 소식 주세요
Note over 서버: 소식이 없다 · 요청을 쥐고 기다린다
Note over 서버: 소식이 생겼다
서버-->>클라이언트: 소식을 담아 응답
클라이언트->>서버: 곧바로 다음 요청
Note over 서버: 정해진 시간이 지나도 소식이 없다
서버-->>클라이언트: 빈 응답
클라이언트->>서버: 곧바로 다음 요청
요청이 풀려나는 길은 둘입니다. 소식이 생기면 서버가 그 소식을 담아 답합니다. 소식 없이 정해진 시간이 지나면 빈 응답으로 답합니다.
어느 쪽이든 클라이언트는 답을 받자마자 다음 요청을 보냅니다. 그래서 서버에는 거의 언제나 쥐고 있는 요청이 하나 있습니다. 소식이 생긴 순간에 그 소식을 실어 보낼 요청이 늘 쥐어져 있는 셈입니다.
겉으로 보면 평범한 HTTP 요청과 응답입니다. 요청 앞머리에 붙는 부가 정보인 헤더도 새로 들지 않습니다. 메시지 형식도 새것이 없습니다. 다른 것은 서버가 답하는 때 하나뿐입니다.
기다림에 끝을 두는 까닭
서버가 요청을 끝없이 쥐고 있지 않는 까닭은 중간에 놓인 장비에 있습니다. 클라이언트와 서버 사이에는 요청을 대신 날라 주는 프록시가 끼는 일이 많습니다.
요청과 응답은 클라이언트와 서버 사이에 맺은 연결 위로 오갑니다. 서버가 요청을 쥐고 있는 동안 그 연결은 아무것도 안 흐른 채 열려 있습니다. 중간 장비는 한동안 아무것도 안 흐르는 연결을 죽은 것으로 보고 끊습니다.
장비가 말없이 끊으면 클라이언트는 그 사실을 늦게 알거나 아예 모릅니다. 그동안 생긴 소식은 실어 보낼 요청이 없습니다. 그래서 서버는 장비가 끊기 전에 스스로 빈 응답을 돌려줍니다. 이 기다림의 상한이 롱 폴링의 타임아웃입니다.
빈 응답을 받은 클라이언트는 곧바로 다시 묻습니다. 그러니 연결이 아무것도 안 흐른 채 머무는 시간은 늘 타임아웃보다 짧습니다. 중간 장비가 그 연결을 죽은 것으로 볼 틈이 없습니다.
클라이언트 쪽 기다림은 서버의 타임아웃보다 조금 길게 잡습니다. 그 시간이 지나도 답이 없으면 연결이 끊긴 것으로 보고 새로 요청합니다.
놓친 소식을 채우는 커서
응답 하나를 받고 다음 요청을 보내기까지 짧은 틈이 있습니다. 그 틈에 생긴 소식은 쥐고 있는 요청이 없어서 곧바로 보낼 곳이 없습니다. 서버가 그 소식을 버리면 클라이언트는 영영 못 받습니다.
그래서 클라이언트는 요청마다 마지막으로 받은 소식의 번호를 실어 보냅니다. 서버는 그 번호 뒤에 생긴 소식을 모아 한꺼번에 돌려줍니다. 이렇게 어디까지 읽었는지를 가리키는 값을 커서라고 부릅니다.
커서가 있으면 연결이 끊겼다 다시 붙어도 끊긴 동안의 소식을 이어서 받습니다. 서버는 이를 위해 최근 소식을 얼마 동안 모아 둬야 합니다.
코드로 본 한 바퀴
클라이언트 쪽 한 바퀴는 반복문 하나입니다. 마지막으로 받은 번호가 41 입니다. 잠시 뒤 42번 소식이 생기는 경우로 봅니다.
cursor = 41
while True:
r = poll(cursor) # 42번이 생길 때 응답
for e in r.events: # [42번] · 초과면 []
handle(e)
cursor = e.id # 42
poll 이 서버에 붙들리는 호출입니다. 서버가 답할 때까지 이 줄에서 멈춰 있습니다. 반복문이 곧바로 다음 poll 을 부르므로 서버에는 늘 기다리는 요청이 하나 있습니다.
기다릴 시간이 다 되어 빈 목록이 오면 for 가 한 번도 안 돕니다. 커서는 41 에 머뭅니다. 같은 번호로 다시 묻습니다.
위 코드에는 없는 경우가 하나 있습니다. 서버가 오류로 답하면 곧바로 다시 묻지 않습니다. 바로 물으면 아픈 서버에 요청을 더 쏟아붓기 때문입니다. 실패가 이어질수록 기다리는 시간을 배로 늘려 가는 지수 백오프를 씁니다.
큐에서 메시지를 기다릴 때
롱 폴링은 브라우저와 서버 사이에서만 쓰는 방식이 아닙니다. 일감을 메시지로 쌓아 두었다가 받는 쪽이 가져가게 하는 메시지 큐에서도 같은 순서를 씁니다.
큐에서 일감을 가져가는 쪽은 「가져갈 것 있나」를 스스로 묻습니다. 큐가 비어 있을 때 곧바로 「없다」를 돌려주면 빈손 요청만 쌓입니다. 그래서 큐가 요청을 쥐고 있다가 메시지가 들어오는 순간 답합니다.
붙들린 요청이 서버에 지우는 짐
롱 폴링은 클라이언트의 헛걸음을 서버의 기다림으로 바꿉니다. 기다리는 클라이언트가 만 명이면 서버는 열린 요청 만 개를 동시에 쥐고 있어야 합니다. 요청이 오면 바로 답하고 놓아 주던 때와는 셈이 다릅니다.
스레드는 한 프로그램 안에서 따로 도는 실행 흐름입니다. 서버가 요청을 처리하는 흔한 방식은 요청마다 스레드를 하나씩 맡기는 것입니다. 롱 폴링에서는 붙들린 요청마다 스레드가 아무 일 없이 잠든 채 묶입니다.
기다리는 클라이언트가 늘면 새 요청을 받을 스레드가 먼저 바닥납니다. 소식은 하나도 안 오갑니다. 그런데도 서버가 새 요청을 못 받는 상태가 됩니다.
그래서 롱 폴링을 받는 서버는 기다림에 스레드를 묶지 않게 짭니다. 스레드 하나가 여러 연결을 번갈아 지켜보다가 준비된 연결만 처리합니다. 이 방식을 논블로킹 입출력이라고 합니다.
서버가 여럿일 때 필요한 중계
서버가 여럿이면 짐이 하나 더 생깁니다. 들어온 요청을 여러 서버로 나눠 주는 로드 밸런서가 앞에 서기 때문입니다.
그러면 소식이 생긴 서버와 그 소식을 기다리는 요청을 쥔 서버가 다를 수 있습니다. 소식은 서버 1 에서 생겼어도 요청은 서버 2 가 쥐고 있는 식입니다. 서버 2 는 서버 1 에서 생긴 일을 모릅니다.
그래서 소식을 모든 서버에 나눠 주는 중계를 따로 둡니다. 소식이 생긴 서버는 중계에 소식을 올립니다. 요청을 쥔 서버가 중계에서 그 소식을 받아 답합니다.
flowchart TD
C["클라이언트"] -->|요청을 보낸다| LB["로드 밸런서"]
LB --> S2["서버 2 · 요청을 쥐고 있다"]
S1["서버 1 · 소식이 생겼다"] -->|올린다| R["중계"]
R -->|나눠 준다| S2
S2 -->|쥐고 있던 요청에 답한다| C
중계가 없으면 서버 1 에서 생긴 소식은 서버 2 에 붙들린 요청으로 가지 못합니다. 올리는 쪽과 받는 쪽을 이렇게 떼어 놓는 방식을 발행-구독이라고 합니다.
한꺼번에 풀려나는 요청
같은 소식을 기다리는 클라이언트가 아주 많을 때도 조심해야 합니다. 소식이 생기면 쥐고 있던 요청이 한꺼번에 풀립니다. 풀려난 클라이언트가 모두 같은 순간에 다음 요청을 보내 서버에 요청이 몰립니다.
한 사건이 기다리던 쪽을 한꺼번에 깨워 부하가 몰리는 현상을 썬더링 허드라고 합니다. 다음 요청을 보내기 전에 클라이언트마다 무작위로 조금씩 기다리게 해서 몰림을 흩뜨리기도 합니다.
다른 실시간 전달 방식과 견주기
서버의 소식을 클라이언트에 전하는 방식은 폴링과 롱 폴링 말고도 둘이 더 있습니다. 표로 견주기 전에 그 둘을 먼저 짚습니다.
Server-Sent Events는 서버가 응답을 끝내지 않고 열어 둔 채 소식을 이어서 흘려보내는 방식입니다. 소식이 서버에서 클라이언트로 한 방향으로만 흐릅니다.
웹소켓은 HTTP 연결을 다른 규칙으로 갈아타서 양쪽이 아무 때나 메시지를 보내게 하는 방식입니다. 연결 하나를 열어 둔 채 오래 씁니다.
넷을 누가 먼저 말을 걸 수 있는지와 요청 하나가 얼마나 오래 사는지로 견주면 이렇습니다.
| 방식 | 누가 말을 거나 | 요청 하나가 사는 동안 | 소식 하나를 받으면 |
|---|---|---|---|
| 폴링 | 클라이언트만 | 곧바로 답을 받고 끝 | 다음 물음 때 받는다 |
| 롱 폴링 | 클라이언트만 | 소식이나 타임아웃으로 답할 때까지 | 응답이 끝나고 다시 묻는다 |
| Server-Sent Events | 서버만 | 닫을 때까지 | 열린 응답에 이어 붙는다 |
| 웹소켓 | 양쪽 다 | 닫을 때까지 | 열린 연결로 곧바로 간다 |
표의 마지막 칸이 롱 폴링이 치르는 대가를 보여 줍니다. 소식 하나마다 응답 하나가 끝나고 새 요청 하나가 생깁니다. 소식이 가끔 오면 문제가 안 됩니다.
소식이 쉴 새 없이 오면 사정이 달라집니다. 요청마다 헤더를 다시 실어 보내야 합니다. 서버도 소식마다 요청을 새로 받아들이는 품을 들입니다.
그 대신 롱 폴링은 평범한 HTTP 요청과 응답만 씁니다. 연결을 갈아타지도 않고 응답을 끝없이 열어 두지도 않습니다. 그래서 망 경계에서 통신을 거르는 방화벽이 HTTP 만 통과시키는 곳에서도 돕니다.
오래된 중간 장비 뒤에서도 마찬가지입니다. 로그인 확인이나 요청 기록처럼 서버에 이미 있는 처리도 손대지 않고 그대로 씁니다.
고르는 물음은 둘입니다. 소식이 잦거나 클라이언트도 자주 말을 걸어야 하면 웹소켓이나 Server-Sent Events 쪽이 덜 수고롭습니다. 소식이 드문드문 오는 화면이 어떤 망에서든 돌아야 하면 롱 폴링이 맞습니다.
관련 항목
롱 폴링과 겨루는 실시간 전달 방식
폴링 · 웹소켓 · Server-Sent Events · Comet · 웹훅 · 푸시 · HTTP 스트리밍
롱 폴링이 올라타는 프로토콜
HTTP · HTTP/1.1 · HTTP/2 · HTTPS · TCP · HTTP 연결 유지
롱 폴링 요청이 지나가는 중간 장비
프록시 · 리버스 프록시 · 로드 밸런서 · 방화벽 · NAT · 게이트웨이
붙들린 요청을 감당하는 서버 구조
스레드 · 스레드 풀 · 논블로킹 입출력 · 이벤트 루프 · 비동기 · 코루틴 · 파일 디스크립터 · C10K 문제
여러 서버 사이에서 소식을 나르는 수단
발행-구독 · 메시지 브로커 · Redis · 수평 확장 · 스티키 세션
롱 폴링으로 메시지를 받아 가는 큐
메시지 큐 · 작업 큐 · Amazon SQS · 경쟁 소비자
기다림과 재시도를 다스리는 장치
타임아웃 · 지수 백오프 · 재시도 · 지터 · 하트비트 · 커서 · 오프셋
롱 폴링에서 터지는 문제
썬더링 허드 · 스레드 고갈 · 메시지 유실 · 중복 전달 · 속도 제한
롱 폴링 요청을 보내는 브라우저 쪽 API
브라우저 · XMLHttpRequest · Fetch API · AJAX
다른 이름: long polling · long-polling · 롱폴링 · 롱 풀링