Socket과 WebSocket 정리 (RFC 6455)
실시간 통신을 개발하면서 Socket과 WebSocket 그리고 Polling과 Long Polling에 대해서 정리하고자 함.
이 글은 RFC 6455 (The WebSocket Protocol) 섹션 1을 기반으로 정리.
목차
- HTTP 정리
- Socket이란?
- 폴링: 계속 물어보는 방식
- 롱폴링: 물어보고 기다리는 방식
- Socket과 WebSocket의 차이
- WebSocket: 하나의 TCP 연결로 양방향
- 오프닝 핸드셰이크: HTTP로 시작해서 HTTP를 떠남
- 클로징 핸드셰이크: TCP FIN만으로 부족한 이유
- TCP, HTTP와의 관계
- 서브프로토콜
- 비교 정리
HTTP 정리
HTTP는 요청-응답(request-response) 모델임.
통신은 언제나 클라이언트의 요청으로 시작하고, 서버는 그 요청에 응답만 할 수 있음.

HTTP 모델의 한계는 서버가 먼저 데이터를 전송할 수 없음.
채팅, 게임, 실시간 시세, 라이브 방송의 시청자 수처럼 서버 쪽에서 이벤트가 발생하는 서비스에서는 이 한계가 그대로 문제가 됨.
RFC 6455의 1.1 Background에서는 WebSocket 이전의 웹이 이 문제를 HTTP 남용(abuse of HTTP)으로 풀어왔다고 지적하며, 그 비용으로 세 가지를 언급함.
- 서버가 클라이언트마다 여러 개의 TCP 연결을 사용하게 됨. 보내는 용도 하나, 받는 용도 하나씩.
- 모든 메시지에 HTTP 헤더가 붙음. 메시지 한 줄을 보내는 데 수백 바이트의 헤더 오버헤드가 발생.
- 클라이언트가 "보내는 연결"과 "받는 연결"의 대응 관계를 직접 추적해야 함.
폴링과 롱폴링은 이 한계 안에서 실시간을 흉내 내는 기법이고, WebSocket은 이 한계 자체를 없애는 프로토콜임.
Socket이란?
WebSocket으로 가기 전에, 이름의 원조인 소켓부터 정리.
소켓(socket)은 운영체제가 제공하는 네트워크 통신 인터페이스임. 프로세스가 네트워크로 데이터를 보내고 받으려면 커널의 네트워크 스택을 거쳐야 하는데, 그 출입구가 소켓.

TCP 연결 하나는 (출발지 IP, 출발지 Port, 목적지 IP, 목적지 Port) 조합으로 식별되고, 소켓은 그 연결의 양 끝단(endpoint)임.
앱 입장에서 소켓은 파일 디스크립터라는 숫자 핸들로 보이며, read, write로 바이트를 흘려보내는 통로.
여기서 중요한 성질이 두 가지 있음.
- 소켓은 프로토콜이 아니라 인터페이스. TCP 위에서 무엇을 주고받을지(HTTP인지, WebSocket인지, 자체 프로토콜인지)는 소켓이 관여하지 않음.
- TCP 소켓은 스트림(stream). 바이트가 순서대로 흐를 뿐, "메시지의 시작과 끝"이라는 경계가 없음. 경계를 나누는 일은 위 계층의 몫.
이 두 성질이 뒤에서 WebSocket과의 차이, 그리고 WebSocket이 프레이밍을 도입한 이유로 이어짐.
폴링: 계속 물어보는 방식
폴링(polling)은 클라이언트가 일정 주기로 서버에 "새로운 것 있어?"라고 묻는 방식.

구현이 가장 단순함. 타이머 하나와 평범한 HTTP 요청이면 충분.
문제는 주기 설정이 딜레마라는 점.
- 주기를 짧게 잡으면: 실시간성은 올라가지만, 대부분의 요청이 빈 응답으로 끝남. 매 요청마다 HTTP 헤더, 경우에 따라 TCP 핸드셰이크와 TLS 협상 비용까지 반복해서 지불.
- 주기를 길게 잡으면: 낭비는 줄지만, 이벤트 발생부터 전달까지 최대 한 주기만큼 지연됨.
즉 폴링에서 실시간성과 자원 효율은 교환 관계임. 어느 쪽으로 조정해도 한쪽을 포기하게 됨.
롱폴링: 물어보고 기다리는 방식
롱폴링(long polling)은 폴링의 "빈 응답" 낭비를 줄이려는 개선.
클라이언트는 똑같이 요청을 보내지만, 서버는 줄 데이터가 없으면 응답을 바로 돌려주지 않고 보류함. 데이터가 생기는 순간 응답하고, 클라이언트는 응답을 받자마자 다음 요청을 다시 걸어둠.

폴링 대비 개선점은 분명함. 빈 응답이 사라지고, 보류 중인 요청이 있다면 이벤트가 발생 즉시 전달됨.
하지만 요청-응답 모델을 벗어난 것은 아님.
- 응답 하나당 전달할 수 있는 이벤트는 한 번. 응답을 받고 다음 요청을 다시 거는 사이에 공백이 생기고, 이벤트가 잦으면 사실상 폴링과 비슷해짐.
- 서버는 응답을 보류하는 동안 클라이언트 수만큼 연결을 계속 점유함.
- 매 사이클마다 HTTP 헤더 오버헤드는 여전히 반복됨.
- 클라이언트에서 서버로 보내는 방향은 여전히 별도의 요청이 필요함. RFC 6455가 지적한 "보내는 연결과 받는 연결의 이중 관리" 문제가 그대로 남음.
정리하면, 롱폴링은 서버에서 클라이언트로 오는 지연을 줄였을 뿐, 양방향 통신 자체를 만든 것은 아님.
Socket과 WebSocket의 차이
소켓은 전송 계층 연결(주로 TCP)의 끝단을 다루는 OS 인터페이스. WebSocket은 그 TCP 소켓 위에서 동작하는 응용 계층 프로토콜. WebSocket 연결도 내부적으로는 결국 TCP 소켓 하나를 사용함.
계층 관계는 아래 이미지와 같음.

구분(TCP) SocketWebSocket
| 정체 | OS가 제공하는 통신 인터페이스 (API) | 표준 문서(RFC 6455)로 정의된 프로토콜 |
| 계층 | 전송 계층 연결의 끝단 | 응용 계층 |
| 데이터 단위 | 바이트 스트림 (경계 없음) | 메시지/프레임 (경계 있음) |
| 연결 수립 | TCP 3-way handshake | TCP 위에서 HTTP Upgrade 핸드셰이크 |
| 사용 주체 | 임의의 네트워크 프로그램 | 주로 웹 환경 (브라우저 포함) |
| 보안 모델 | 없음 (앱이 알아서) | origin 기반 보안 모델 내장 |
즉 "Socket vs WebSocket"은 대등한 비교가 아니라 층이 다른 개념임.
WebSocket을 이해한다는 것은 "TCP 소켓 위에 어떤 규칙을 얹었는가"를 이해하는 일.
WebSocket: 하나의 TCP 연결로 양방향
RFC 6455가 정의하는 WebSocket은 폴링, 롱폴링이 우회하던 문제를 정면으로 풂.
핵심은 단 하나의 TCP 연결 위에서 클라이언트와 서버가 대등하게, 아무 때나, 양방향으로 메시지를 보내는 것.프로토콜은 크게 두 부분으로 구성됨 (RFC 6455 §1.2).
- 핸드셰이크: HTTP 형식을 빌린 연결 수립 절차
- 데이터 전송: 연결 수립 후, 프레임(frame) 단위로 메시지를 주고받는 단계
이 구조 덕분에 폴링의 세 가지 비용이 모두 사라짐. 연결은 하나고, 메시지마다 붙는 것은 HTTP 헤더가 아니라 몇 바이트짜리 프레임 헤더이며, 보내는 연결과 받는 연결을 구분해서 관리할 필요가 없음.
오프닝 핸드셰이크: HTTP로 시작해서 HTTP를 떠남
WebSocket 연결은 평범한 HTTP GET 요청으로 시작함. RFC 6455 §1.2의 예시 그대로.
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
서버가 WebSocket을 지원하면 아래처럼 응답함.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
상태 코드 101 Switching Protocols가 교환되는 순간, 이 TCP 연결 위의 대화는 더 이상 HTTP가 아님. 같은 연결이 그대로 WebSocket 프레임을 실어 나르는 통로가 됨.
굳이 HTTP 형식으로 시작하는 이유는 호환성 (§1.3). 80/443 포트, HTTP 서버, 프록시 같은 웹의 기존 인프라를 그대로 통과하기 위해, 이미 웹이 이해하는 문법인 HTTP Upgrade 메커니즘을 입구로 삼음.
각 헤더의 역할:
- Upgrade: websocket + Connection: Upgrade: 이 연결을 WebSocket으로 전환하겠다는 선언
- Sec-WebSocket-Key: 16바이트 난수를 base64 인코딩한 값 (다음 절에서 정리)
- Sec-WebSocket-Version: 13: 프로토콜 버전. RFC 6455가 13
- Origin: 브라우저 환경에서 요청 출처를 서버에 알려주는 값. 승인되지 않은 웹 페이지의 크로스 오리진 연결을 서버가 거부할 수 있게 함
- Sec-WebSocket-Protocol: 서브프로토콜 협상 (뒤에서 정리)
클로징 핸드셰이크: TCP FIN만으로 부족한 이유
TCP에도 이미 연결 종료 절차(FIN/ACK)가 있음. 그런데 WebSocket은 그 위에 자체 종료 핸드셰이크를 하나 더 정의함 (§1.4).
절차는 단순함. 한쪽이 Close 프레임을 보내고, 상대가 Close 프레임으로 응답하면, 먼저 보낸 쪽이 TCP 연결을 닫음. 양쪽이 동시에 시작해도 됨.
TCP 종료를 그대로 쓰지 않는 이유를 RFC는 아래처럼 설명함.
- TCP의 종료 신호는 프록시 같은 중간 장치를 거치는 환경에서 끝(end-to-end)까지 항상 신뢰성 있게 전달된다고 보장할 수 없음.
- 응용 계층에서 명시적으로 Close를 주고받으면, 양쪽 모두 "상대가 내 마지막 데이터까지 받았고, 연결이 의도적으로 끝났다"는 사실을 확실히 알 수 있음. 갑작스러운 연결 유실과 정상 종료를 구분할 수 있게 됨.
Close 프레임을 받은 뒤에 도착하는 데이터는 폐기됨.
TCP, HTTP와의 관계
WebSocket은 TCP 위에서 동작하는 독립적인 프로토콜. HTTP와의 관계는 핸드셰이크가 HTTP 서버에게 Upgrade 요청으로 해석.
- 핸드셰이크가 끝난 뒤의 WebSocket 트래픽은 HTTP가 아님. HTTP의 메서드, 상태 코드, 헤더 같은 개념은 더 이상 등장하지 않음.
- 기본 포트는 HTTP와 같음. ws://는 80, TLS 위에서 동작하는 wss://는 443. 기존 웹 인프라와 공존하기 위한 선택.
- 같은 포트를 HTTP 서버와 공유할 수 있음. 이 경우 WebSocket 연결 시도는 서버 입장에서 "Upgrade 헤더가 붙은 평범한 GET 요청"으로 보임 (§1.8).
- RFC는 당시 기준으로 443 포트(wss)의 연결 성공률이 80 포트보다 유의미하게 높았다고 언급함. 중간 장치들이 TLS 트래픽에는 개입하지 못하기 때문.
서브프로토콜
WebSocket은 메시지의 경계와 타입(텍스트/바이너리)까지만 정의함. 그 메시지 안에 무엇을 어떤 형식으로 담을지는 프로토콜이 관여하지 않음.
그 "안의 규칙"을 협상하는 장치가 서브프로토콜 (§1.9).
클라이언트는 핸드셰이크에서 사용 가능한 서브프로토콜 목록을 제시하고, 서버는 그중 하나를 선택해서 응답함.
클라이언트: Sec-WebSocket-Protocol: chat, superchat (이 중에 골라줘)
서버: Sec-WebSocket-Protocol: chat (chat으로 하자)
RFC가 권장하는 관례는 두 가지.
- 이름 충돌을 피하기 위해, 서브프로토콜 이름에 만든 주체의 도메인을 포함. 예: chat.example.com
- 버전 관리: 기존 클라이언트와 호환되지 않는 변경이라면 새 이름을 사용 (예: v2.chat.example.com). 같은 이름을 유지하며 확장하려면 하위 호환을 신중히 설계해야 함.
STOMP 같은 메시징 프로토콜이 WebSocket 위에서 동작하는 것이 바로 이 서브프로토콜 계층의 활용.
비교 정리
| 구분 | 폴링 | 롱폴링 | WebSocket |
| 통신 모델 | 요청-응답 반복 | 요청-응답 (응답 보류) | 양방향 (full-duplex) |
| 서버 → 클라이언트 지연 | 최대 폴링 주기만큼 | 거의 즉시 (요청 보류 중일 때) | 즉시 |
| 클라이언트 → 서버 | 별도 요청 | 별도 요청 | 같은 연결로 즉시 |
| 메시지당 오버헤드 | HTTP 헤더 전체 | HTTP 헤더 전체 | 프레임 헤더 몇 바이트 |
| 빈 응답 낭비 | 있음 (주기가 짧을수록 큼) | 없음 | 없음 |
| 연결 관리 | 요청마다 새로 (keep-alive 여부에 따라 다름) | 사이클마다 반복 | 하나의 TCP 연결 유지 |
| 서버 부담 | 잦은 요청 처리 | 대기 연결 다수 점유 | 연결당 상태 유지 |
| 구현 복잡도 | 낮음 | 중간 | 프로토콜/라이브러리 필요 |
| 인프라 호환성 | 어디서나 동작 | 어디서나 동작 (타임아웃 주의) | 중간 장치에 따라 차단될 수 있음 (wss 권장) |
- 폴링: 갱신 주기가 길고 실시간성이 중요하지 않은 데이터에 적합. 예를 들어 몇 분 단위의 상태 확인. 구현과 운영이 가장 단순하고, 어떤 인프라에서도 동작함.
- 롱폴링: 실시간성이 필요하지만 WebSocket을 쓸 수 없는 환경의 폴백(fallback). SockJS 같은 라이브러리가 WebSocket 실패 시 롱폴링으로 내려가는 전략을 쓰는 이유.
- WebSocket: 양방향 메시지가 잦은 경우. 채팅, 실시간 게임, 협업 편집, 시세 스트리밍처럼 서버 푸시와 클라이언트 전송이 모두 빈번한 서비스.
판단 기준을 하나로 줄이면 이렇게 됨. 서버가 먼저 클라이언트에게 데이터를 주어야 하는 빈도가 높고 양방향인가이라면 WebSocket, 아니라면 더 단순한 쪽이 나음.
- 소켓은 인터페이스, WebSocket은 프로토콜. WebSocket도 결국 TCP 소켓 하나 위에서 동작함.
- HTTP의 근본 한계는 서버가 먼저 말을 걸 수 없다는 것. 폴링과 롱폴링은 그 한계 안의 우회책.
- WebSocket 핸드셰이크는 HTTP로 시작하지만, 101 응답 이후 그 연결은 더 이상 HTTP가 아님.
- Sec-WebSocket-Key와 Accept는 암호화나 인증이 아니라 "상대가 WebSocket을 이해했는가"의 검증.
- WebSocket이 TCP에 얹은 것은 네 가지뿐. origin 보안 모델, 주소 지정, 프레이밍, 클로징 핸드셰이크.
- 메시지 안의 형식은 서브프로토콜의 몫.


(참고)
'백엔드' 카테고리의 다른 글
| Express req.query와 query parser (0) | 2026.07.07 |
|---|---|
| Express app.param vs app.get (0) | 2026.07.07 |
| Message Broker 개념 정리와 Kafka, RabbitMQ 비교 (0) | 2025.11.05 |
| Node.js 몽고디비 알아보기 (0) | 2023.08.10 |
| Node.js MySQL 다뤄보기 (0) | 2023.07.23 |