29강
웹소켓
브라우저가 계속 묻지 않아도 서버가 먼저 말할 수 있게 하는 방법
먼저 보면 좋은 강의: 웹 요청의 해부, 이벤트 기반 아키텍처
단계별로 보기
스페이스바로 재생/정지, 좌우 화살표로 단계 이동, R로 처음으로 돌아갑니다.
대기 중1 / 4 단계 — 계속 물어본다
브라우저가 5초마다 묻습니다. 새 거 있나요. 없음. 새 거 있나요. 없음. 질문 예순 번 중 쉰아홉 번이 헛수고입니다. 그런데도 서버는 그 질문을 전부 받아내야 합니다. 사용자가 십만 명이면 초당 이만 건입니다. 의미 있는 응답은 그중 몇 건뿐입니다.
이 강의를 마치면
- HTTP가 '묻고 답하는' 구조라서 서버가 먼저 말할 수 없다는 점을 설명한다.
- 폴링이 낭비를 만드는 이유를 빈도와 지연의 맞교환으로 설명한다.
- 연결을 열어두는 방식이 무엇을 해결하고 무엇을 새로 요구하는지 설명한다.
- 서버가 사용자에게 먼저 알리는 경로를 직접 구성한다.
서버는 먼저 말할 수 없다
웹의 기본 규칙은 단순합니다. 브라우저가 묻고, 서버가 답합니다. 항상 이 순서입니다. 서버가 먼저 "야, 새 메시지 왔어"라고 말할 방법이 없습니다.
평소에는 문제가 안 됩니다. 페이지를 보고 싶으면 브라우저가 요청하면 되니까요. 그런데 채팅, 알림, 실시간 시세처럼 서버 쪽에서 먼저 일이 생기는 경우에는 이 규칙이 곧바로 걸림돌이 됩니다.
서버는 새 메시지가 도착한 걸 압니다. 그런데 말할 방법이 없습니다.
방법 1 — 계속 물어본다 (폴링)
가장 단순한 우회로입니다. 브라우저가 5초마다 물어봅니다. "새 거 있나요?"
브라우저: 새 거 있나요? → 서버: 없음
(5초 뒤) 새 거 있나요? → 서버: 없음
(5초 뒤) 새 거 있나요? → 서버: 없음
(5초 뒤) 새 거 있나요? → 서버: 있음! 여기요동작은 합니다. 하지만 대가를 보세요.
거의 전부가 헛수고입니다. 채팅 메시지가 5분에 한 번 온다면, 60번 물어서 59번은 "없음"을 받습니다. 98%가 낭비입니다. 사용자 10만 명이 5초마다 물으면 서버는 초당 2만 건을 받습니다. 그중 의미 있는 응답은 몇 건뿐인데도 그 부하를 감당할 서버를 사서 유지해야 합니다.
그런데도 느립니다. 메시지가 방금 도착해도 사용자는 최대 5초 뒤에 압니다.
여기에 폴링의 딜레마가 있습니다.
| 물어보는 주기 | 서버 부하 | 사용자가 늦게 아는 시간 |
|---|---|---|
| 1초마다 | 매우 높음 | 최대 1초 |
| 5초마다 | 높음 | 최대 5초 |
| 60초마다 | 낮음 | 최대 60초 |
부하와 지연을 맞바꾸는 것 말고는 할 수 있는 게 없습니다. 둘 다 좋게 만들 수 없습니다. 구조 자체가 그렇게 생겼기 때문입니다.
방법 2 — 전화를 끊지 않는다 (웹소켓)
발상을 뒤집습니다. 매번 새로 전화를 걸어 "새 거 있나요"를 묻는 대신, 한 번 걸어놓고 끊지 않습니다.
연결이 열려 있는 동안에는 양쪽 모두 아무 때나 말할 수 있습니다. 브라우저도 말하고, 서버도 먼저 말합니다. 묻고 답하는 순서가 사라집니다.
이제 이렇게 됩니다.
브라우저: (연결을 연다) ─────────────── 계속 열려 있음 ───────────────
(아무 일도 없음. 요청 0건. 부하 0)
...
서버: 새 메시지 왔어요! ← 서버가 먼저 말한다. 도착 즉시.헛된 질문이 사라졌습니다. 그리고 지연도 사라졌습니다. 폴링에서 맞바꿔야 했던 두 가지를 한꺼번에 해결했습니다.
대가는 무엇인가
물론 공짜가 아닙니다. 이번 대가는 기억해야 할 것이 생긴다는 점입니다.
일반 HTTP 서버는 요청을 처리하고 나면 그 사용자를 잊어버립니다. 그래서 서버를 아무거나 골라 보내도 되고, 늘리고 줄이기가 쉬웠습니다.
웹소켓 서버는 그럴 수 없습니다. 누가 연결되어 있는지 계속 기억해야 합니다. 여기서 골치 아픈 문제가 따라옵니다.
사용자 A는 서버 1번에 연결되어 있습니다. 그런데 A에게 보낼 메시지가 서버 2번에서 생겼습니다. 서버 2번은 A에게 어떻게 말할까요?
서버 2번은 A와 연결되어 있지 않습니다. 직접 말할 방법이 없습니다. 그래서 서버끼리 소식을 주고받을 통로가 필요합니다. 여기서 앞 강의의 큐가 다시 등장합니다. 메시지를 큐에 발행하면, 연결을 쥐고 있는 서버가 그것을 받아 자기 사용자에게 전달합니다.
즉 웹소켓을 쓰면 뒤쪽에 메시지 전달 구조가 거의 반드시 필요해집니다. 이것이 실시간 기능의 진짜 비용입니다. 연결 자체가 아니라, 연결을 여러 서버에 흩어놓았을 때 생기는 문제입니다.
언제 쓰는가
- 폴링으로 충분한 경우: 변화가 드물고 몇 초 늦어도 되는 경우. 대부분이 여기입니다.
- 웹소켓이 필요한 경우: 채팅, 협업 편집, 실시간 시세처럼 **서버가 먼저 말해야 하고,
지연이 곧 품질인** 경우.
실시간이 멋있어 보인다는 이유로 고르는 것은 대개 비쌉니다.
이 강의가 단순화한 것
교육용 모형은 언제나 무언가를 생략합니다. 무엇을 생략했는지 아는 것도 학습의 일부입니다.
전제한 것
- 이 시뮬레이터에는 웹소켓 전용 컴포넌트가 없다. 그래서 연결을 유지하는 서버를 일반 API 서버로, 서버 간 메시지 전달을 큐와 워커로 표현한다. 화면의 연결선이 HTTP로 표시되는 것은 이 때문이며, 실제 웹소켓은 별도의 프로토콜이다.
- 연결을 여는 데 드는 비용(핸드셰이크)과 연결을 유지하는 데 드는 메모리는 이 모형에서 다루지 않는다. 실제로는 이것이 서버당 동시 연결 수의 상한을 정한다.
- 이 강의의 폴링 부하 수치(예: 98% 낭비)는 '5분에 한 번 메시지가 온다'는 가정에서 계산한 예시이며 실제 측정값이 아니다.
- 연결이 끊어졌을 때의 재연결과 그 사이 놓친 메시지 처리는 다루지 않는다.
다루지 않은 것
- 웹소켓 핸드셰이크(HTTP Upgrade)와 프레이밍
- 서버-전송 이벤트(SSE)와 롱 폴링 등 중간 형태
- 연결 상태를 유지한 채 서버를 늘리고 줄이는 문제
- 재연결·하트비트·백프레셔
이제 직접 만들어 보세요
읽어서 아는 것과 만들어서 아는 것은 다릅니다. 컴포넌트를 배치하고 트래픽을 흘려서 실제로 동작하는지 확인해 보세요.
이 강의에 물어보기
하루 5번이 강의 본문만 근거로 답합니다. 강의에 없는 내용은 지어내지 않습니다.