웹소켓 (WebSocket)

"The WebSocket Protocol enables two-way communication between a client running untrusted code in a controlled environment to a remote host."

— IETF RFC 6455 표준 문서 서문 중.

"클라이언트와 서버 사이에 지연 시간(Latency)이 거의 없는 무한의 아우토반을 깔아주는 마법의 터널. 하지만 그 터널의 커넥션 상태를 완벽히 감시하고 유지하는 비용은 오롯이 개발자의 눈물과 서버 자원으로 청구된다." 실시간 알림 하나 띄우겠다고 무지성으로 소켓을 여는 주니어를 보면 등 뒤에서 식은땀이 흐른다. 제발 얌전히 HTTP 폴링이나 쓰렴.(...)

1. 개요

웹소켓은 단일 TCP 커넥션을 통해 웹 브라우저와 서버 간에 완전한 전이중(Full-Duplex) 양방향 실시간 통신을 제공하는 네트워크 프로토콜이다.

과거 HTTP가 가지고 있던 "클라이언트가 요청해야만 서버가 대답한다"는 철저한 갑을 관계를 완전히 뒤엎고, 서버가 언제든 먼저 클라이언트의 뺨을 때려 깨우며 데이터를 던질 수 있는 동등한 동반자 관계를 열었다. 실시간 주식 HTS, 가상화폐 거래소 전광판, 웹 기반 멀티플레이어 게임 및 실시간 채팅을 구현할 때 사실상 고정 주전으로 등판하는 독보적인 기술이다.(...)

2. 양방향 실시간 통신의 정수, Handshake

2.1. 웹소켓 프로토콜의 작동 원리 (The Handshake)

웹소켓은 처음부터 독자적으로 소켓을 여는 것이 아니라, 초기 연결 시에는 친숙한 HTTP 프로토콜의 도움을 받는다. 이를 웹소켓 핸드셰이크(WebSocket Handshake)라 칭한다.

  1. 클라이언트가 서버로 일반 HTTP 요청을 보내며, 헤더에 "Upgrade: websocket""Connection: Upgrade"를 얹어 보낸다. 이는 쉽게 말해 "우리 이제 HTTP 말고 한 차원 높은 대화를 나누자"라는 수줍은 고백이다.
  2. 서버가 이 요청을 수락하면 101 Switching Protocols 상태 코드로 응답하며, 이 순간 TCP 커넥션은 웹소켓 프로토콜(ws:// 또는 wss://)로 완벽하게 신분 상승을 하게 된다.1
  3. 핸드셰이크가 끝나면 HTTP의 오버헤드가 큰 헤더(Header)들은 전부 사라지고, 오직 본문 데이터만 아주 작고 압축된 바이너리/텍스트 프레임(Frame) 단위로 쉴 새 없이 주고받는다.

2.2. 보안을 위한 필수 조건: wss 프로토콜

일반 HTTP에 HTTPS가 필수이듯, 웹소켓 역시 평문으로 데이터를 보내는 ws://는 실무에서 봉인해야 한다. 반드시 SSL/TLS 암호화가 적용된 wss:// 프로토콜을 사용해야 프록시 서버나 방화벽의 패킷 필터링 필터를 통과할 수 있으며, 중간 해커가 채팅 내용을 훔쳐보는 참사를 막을 수 있다.

3. 웹소켓 분산 아키텍처의 한계와 고통

3.1. 소켓 서버 스케일아웃(Scale-out)의 장벽

일반 HTTP API 서버는 무상태(Stateless)이므로 로드밸런서 뒷단에 서버를 수백 대 갖다 놔도 아무 상관이 없다. 그러나 웹소켓은 유상태(Stateful) 연결이다. 특정 브라우저가 서버 A와 연결된 소켓을 물고 있다면, 그 상태가 영구 유지된다. 이 상황에서 서버 B와 연결되어 있는 다른 사용자에게 메시지를 보내고 싶다면 어떻게 해야 할까? 서버 A와 B가 서로 소켓의 상태를 전혀 모르므로 메시지 전달이 불가능해진다. 이 때문에 분산 환경에서는 결국 Redis의 Pub/Sub 메커니즘을 어댑터로 중간에 껴서 모든 소켓 서버 간에 메시지를 실시간으로 퍼뜨리는 "소켓 메시지 버스" 아키텍처를 강제로 구현해야 한다.2

3.2. 좀비 커넥션과 핑퐁(Ping-Pong) 전쟁

인터넷망은 생각보다 불안정해서 사용자의 랜선이 갑자기 뽑히거나 와이파이가 끊겼을 때, 서버는 해당 사용자와의 소켓이 죽었는지 살았는지 즉시 알지 못한다. 이를 방치하면 메모리 상에 쓸모없는 좀비 커넥션이 무한히 쌓여 서버가 폭사한다. 이를 방지하기 위해 서버와 클라이언트는 백그라운드에서 주기적으로 "살아있니?"(Ping) "응 살아있어"(Pong)라는 기묘한 핑퐁 패킷을 계속 날려야 하며, 일정 시간 응답이 없으면 소켓을 무자비하게 찢고 연결을 정리해야 한다.3

4. 웹소켓 실무자들의 피눈물 밈

4.1. 실시간 알림용 무지성 소켓

주니어 개발자들이 "어? 실시간으로 알림 종 모양 배지에 빨간 불 들어오게 해야 하는데요?"라며 무작정 웹소켓 서버를 뚫으려 덤벼든다. 하지만 동접자가 10만 명으로 치솟는 순간, 서버 포트 자원이 고갈되고 메모리가 마비되는 참사를 직면한다. 실무자들은 "그냥 5분 주기로 HTTP 롱 폴링(Long Polling)을 하거나 SSE 단방향 스트림으로 밀어 넣으면 해결될 일을 굳이 양방향 광선검을 휘두르다 제 발등을 찍었다"며 한탄하곤 한다.(...)

5. 여담

  • Socket.io의 환각: 수많은 Node.js 개발자들은 자바스크립트의 유명 실시간 라이브러리인 Socket.io가 곧 웹소켓인 줄 착각하지만, 사실 Socket.io는 웹소켓이 지원되지 않는 레거시 브라우저를 위해 HTTP 폴링 등으로 우회(Fallback)하는 기술까지 몽땅 감싸놓은 덩치 큰 프레임워크다. 현대 브라우저는 100% 순수 웹소켓(Native WebSocket)을 지원하므로 단순 소켓은 라이브러리 없이 순수로 짜는 게 백배 가볍다.
  • 서버 포트 65535의 한계 극복: 리눅스에서 TCP 포트 개수는 최대 65,535개로 제한되어 있어 초보자들은 "소켓 서버 한 대로는 최대 6만 명만 동시 접속할 수 있겠네?"라고 생각한다. 하지만 TCP 커넥션은 [출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트]의 4중 튜플 조합으로 식별되기 때문에, 서버 세팅만 잘 풀어주면 단일 포트 80에서 수십만 개의 실시간 웹소켓 연결을 물고 버틸 수 있다.
  • 메모리 학살의 원흉: 소켓 연결 하나당 차지하는 메모리 버퍼 크기는 작게는 수 KB에서 수십 KB에 달한다. 만약 접속자 수십만 명이 접속해 활발하게 메시지를 흩뿌리면 소켓 버퍼 메모리만 수십 GB를 갉아먹기 때문에 소켓 전용 인스턴스는 고성능 램 세팅이 강제된다.

6. 관련 문서

각주

  1. 핸드셰이크 응답의 Sec-WebSocket-Accept 헤더는 클라이언트가 보낸 난수 키값에 특정 매직 문자열을 합치고 SHA-1 해싱하여 생성된 특수 암호 키다. 이로써 정상적인 핸드셰이크가 이루어졌음을 확실히 보장한다.

  2. 실제로 대형 게임사나 실시간 협업 툴 슬랙(Slack) 등의 백엔드는 이 분산 소켓 동기화 처리에만 엔지니어링 리소스의 절반 이상을 쏟아붓는다.

  3. 브라우저 단에서는 EventSource처럼 자동 재연결이 기본 탑재되어 있지 않기 때문에, 자바스크립트로 직접 onclose 핸들러에 재연결 로직을 한 땀 한 땀 구현하지 않으면 인터넷이 잠깐만 튕겨도 실시간 채팅창이 완전히 죽어버리는 대형 사고가 터진다.