March 20, 2026

HTTP와 WebSocket

들어가며

HTTP와 WebSocket(이하, 웹소켓)은 단순히 실시간이냐 아니냐의 차이가 아니라 구조와 오버헤드, 상태 관리 측면에서 구분해야 한다.

HTTP

HTTP는 클라이언트와 서버가 메시지를 주고받는 약속이며, Request-Response 모델과 무상태성(Stateless)이 핵심이다.

한 번의 요청-응답이 끝나면 서버는 클라이언트와 연결을 끊고 이전 상태를 기억하지 않는다. 때문에 서버의 확장성에 유리하지만, 실시간성이 중요한 서비스에 한계가 있다.

무상태성은 확장에 도움이 된다.

HTTP는 서버의 수평적 확장성에 최적화되어 있다. 이는 대규모 시스템을 빌딩할 때 유리한 핵심 요인이다.

HTTP를 이용하면 클라이언트와 서버 간에 컨텍스트를 유지하지 않는다. 이는 서버가 세션 상태를 메모리에 들고 있지 않으므로, 클라이언트의 요청이 어떤 서버로 전달되더라도 항상 동일한 응답을 받을 수 있음을 의미한다. 이 특징은 로드 밸런서 같은 부하 분산 서비스에 그대로 드러난다. 로드 밸런서는 수십, 수백 대의 서버로 요청을 자유롭게 분산해도 클라이언트에게 동일한 응답을 제공할 수 있다.

🧐 세션 클러스터링이나 스티키 세션을 사용하고 있다면, HTTP 무상태성을 제약할 수 있고 이는 확장성 저해 요인이 된다.


또한, HTTP는 강력한 캐싱 메커니즘이 내장되어 있다. Cache-Control, ETag, Last-Modified와 같은 헤더를 통해 응답 데이터를 재사용할 수 있다. 요청이 서버까지 도달하기 전에 브라우저나 CDN, 프록시 서버에서 처리되므로 서버의 부하를 획기적으로 줄인다. 이는 서버 자원을 증설하지 않고도 더 많은 트래픽을 처리할 수 있게 한다.

💡 앞서 언급한 로드밸런서, 프록시 서버 모두 HTTP에 의한 계층화된 시스템이다. 클라이언트는 자신이 통신하는 대상이 최종 서버인지, 중간 프록시인지 전혀 알 필요 없기 때문에 시스템을 중단하지 않고 특정 계층을 독립적으로 확장하거나 변경할 수 있다.


마지막으로 URI를 통해 자원을 식별하고, 표준 메서드(GET, POST, PUT, DELETE)를 사용하는 일관된 인터페이스 덕분에 시스템 결합도를 낮춘다. MSA에서도 각 서비스가 동일한 HTTP 표준을 따르기 때문에 서로 다른 기술 스택으로 구성된 서비스 간에도 통신이 원할하며 개별 서비스 단위로 독립적 확장이 가능해 진다.


핸드셰이크

엄밀히 말해 HTTP가 전송 계층과 보안 계층에서 신뢰성 있는 연결을 맺기 위해 의존하는 TCP 3-Way Handshake와 TLS Handshake의 결합된 과정을 의미한다.

HTTP는 TCP 위에서 동작한다. 따라서 데이터를 주고받기 전에 TCP 연결이 선행되어야 한다. 이 과정은 클라이언트와 서버가 서로의 존재를 확인하고 패킷 순서를 보장하기 위한 시퀀스 번호를 동기화하는 작업이다.

🤔 왜 패킷 순서를 보장해야 할까?

  • IP(Internet Protocol)는 비신뢰성, 비연결성 특징을 갖는다. IP 계층은 패킷을 목적지까지 전달하는 데만 최선을 다할 뿐, 어떤 경로로 가는지나 어떤 순서로 도착하는지는 전혀 책임지지 않는다.
  • 네트워크 혼잡 상황에서 1번 패킷은 직진, 2번 패킷은 우회로를 택한다면 2번이 먼저 도착할 수도 있다.
  • 만약 TCP가 이 순서를 맞춰주지 않는다면, 우리가 전송한 데이터(이미지, 파일 등)는 앞뒤가 뒤섞여 '손상된 파일’로 인식된다. (엄격한 규칙을 가진 Byte Stream이기 때문. 반면에 실시간 비디오 스트리밍처럼럼 순서를 맞추는 비용보다 지연 시간을 줄이는 이득이 더 큰 경우도 있음)

tcp 3 way handshake

  1. 클라이언트가 서버에 연결 요청을 의미하는 SYN 패킷을 보낸다. 이때 클라이언트는 임의의 초기 시퀀스 번호를 생성해 전달한다.
  2. 서버는 요청을 수락하며 SYN-ACK 패킷으로 응답한다. 서버 역시 자신의 초기 시퀀스 번호를 생성하고, 클라이언트의 번호에 1을 더해 확인 응답을 보낸다.
  3. 마지막으로 클라이언트가 다시 서버의 번호에 1을 더한 ACK 패킷을 보내면 비로소 양방향 통신이 가능한 ESTABLISHED 상태가 된다.

HTTPS에선 TCP 연결 직후 TLS(Transport Layer Security) Handshake가 이어진다. 목적은 서버의 신원을 확인하고, 데이터를 암호화할 대칭키를 안전하게 공유하기 위해서다. [1]

tls handshake

  1. Client Hello → 클라이언트가 지원 가능한 암호화 방식(Cipher Suite), TLS 버전, 클라이언트 난수 등을 서버에 보낸다.
  2. 서버가 사용할 암호화 방식을 선택해 알리고, 자신의 공개키가 담긴 SSL/TLS 인증서를 전달한다. ← Server Hello & Certificate
  3. 서버가 필요한 정보를 모두 보냈음을 알린다. ← Server Key Exchange / Hello Done
  4. Client Key Exchange → 클라이언트는 서버의 인증서를 검증하고 Pre-Master Secret이란 난수 값을 생성해 서버의 공개키로 암호화해 보낸다.
  5. Change Cipher Spec/Finished → 양측은 교환한 난수들을 조합해 동일한 대칭 키(Session Key)를 생성한다. 이후 이제부터 이 키로 암호화해 보냄을 선언하며 핸드셰이크를 종료한다. ← Change Cipher Spec/Finished

WebSocket

HTTP는 비연결성(Stateless) 구조기 때문에 클라이언트의 요청이 있어야 서버가 응답을 줄 수 있는 반면, 웹소켓은 전이중(Full-duplex) 통신 채널을 제공하는 프로토콜이다. 따라서 상태를 유지하며 클라이언트와 서버간 연결이 한 번 확립되면 명시적으로 끊지 않는 한 유지된다. 또한 서버가 먼저 데이터를 보낼 수 있다.

HTTP 기반의 핸드셰이크 + Upgrade

웹소켓 연결은 HTTP 기반의 핸드셰이크를 기반으로 한다.

💡 웹소켓은 항상 HTTP, HTTPS 연결을 업그레이드해 구현된다. [2]

클라이언트가 HTTP 요청 헤더에 Upgrade: websocketConnection: Upgrade를 담아 보내면, 서버는 이를 수용해 101 Switching Protocols 응답을 보낸다. [3] 필요한 경우 추가적인 핸드셰이크 과정을 거친다.

💡 업그레이드할 수 없는 경우, Upgrade 헤더는 무시되고 200 OK 일반 응답을 보낸다.

💡 101 Switching Protocols[4]란 응답 코드의 한 종류로, "서버가 전환되는 프로토콜"을 가리킨다. 프로토콜은 클라이언트 요청에서 받은 Upgrade 헤더에 명시되어 있으며, 서버는 이 응답에 전환된 프로토콜을 나타내는 Upgrade 헤더를 포함한다.

그럼 기존의 HTTP 프로토콜은 웹소켓 프로토콜로 전환되며, 동일한 TCP 포트(80 또는 443)를 공유하면서도 전혀 다르게 작동한다.

웹소켓 취약점이 발생하는 이유

이 과정에서 보안과 호환성을 챙겨야 하며, ws:// 혹은 보안이 강화된 wss:// 스킴을 사용하며, 브라우저의 동일 출처 정책(Same Origin Policy)을 따르지 않기 때문에 서버 사이드에서 별도로 CORS 설정이나 인증 로직을 설계하고 구현해야 한다.

💡 SOP란 브라우저의 가장 기본적인 보안 메커니즘중 하나이다. "A라는 사이트에서 가져온 스크립트는 B라는 사이트의 데이터에 접근할 수 없다"는 규칙이다. 여기서 출처(Origin)는 프로토콜, 호스트(도메인), 포트가 모두 같아야함을 의미한다. 만약 SOP가 없다면, 악의적인 사이트가 브라우저에 저장된 사용자의 은행 세션 정보를 빼가는 등의 공격이 가능해진다.

💡 CORS(Cross Orgin Resource Sharing)란 SOP의 제약을 안전하게 해결하기 위한 메커니즘이다. 실제 개발을 하다보면 프론트엔드 도메인과 API 서버 도메인이 달라서 SOP가 발생하게 되는데, 이때 서버가 응답 헤더 Access-Control-Allow-Origin특정 외부 출처는 내 데이터를 가져가도 된다라고 명시해 주는 설정이다.

웹소켓은 태생부터 크로스 도메인을 지향했다. 때문에 크로스 도메인 요청이 가능하다. 그래서 서버가 반드시 핸드셰이크 요청이 올 때 Origin 헤더를 검증해서, 의도하지 않은 도메인에서 오는 WebSocket 연결을 막아야 한다. 이를 제대로 안 하면 Cross-Site WebSocket Hijacking 공격이 가능하다. [5]

💡 Cross-Site WebSocket Hijacking은 공격자가 사용자 위치에 저장된 인증 쿠키를 이용해 evil.com에서 var ws = new WebSocket("wss://bank.com/chat"); 같은 명령을 수행하게 해 데이터를 가로채거나 조작하는 공격이다.

💡 Origin 헤더 검증 외에 SameSite 쿠기 속성을 사용할 수도 있다. 쿠키를 설정할 때 SameSite=Lax 또는 Strict 설정을 하면, 브라우저는 제3의 사이트(Cross-Site)에서 발생하는 요청에는 쿠키를 실어 보내지 않는다. 또한 추가 방어선으로 CSRF Token 검증, Authorization 헤더(JWT 등)을 사용한 방식도 있다.


HTTP와 웹소켓의 트레이드오프

HTTP/1.1은 매 요청마다 헤더 정보를 포함한 데이터를 주고 받는다.

실시간성을 흉내내려면 Polling이나 Long Polling 기법을 사용한다. 이때, 서버 자원 낭비와 지연 시간 문제를 야기한다.

이 두 기법은 서버가 먼저 데이터를 줄 수 없는 HTTP의 한계를 극복하기 위한 고육지책이다.

Polling은 클라이언트가 일정한 시간 간격으로 서버에 되묻는 방식이다. 구현이 매우 단순하고 무상태성을 유지할 수 있지만, 실시간성은 다소 떨어진다. 데이터가 발생한 직후가 아닌 다음 요청 주기까지 기다려야 하기 때문이다.

또한, 데이터 변화가 없더라도 계속 요청을 해야해서 네트워크 대역폭과 서버 자원(CPU, Memory) 낭비가 심할 수 있다. 이 경우 매 요청마다 수백 바이트의 HTTP 헤더가 포함되는 것이 큰 부담으로 작용한다.

Long Polling은 Polling의 비효율성을 개선했다. 클라이언트가 요청을 보내면 서버가 데이터가 생길 때까지 혹은 타임아웃이 될 때까지 응답을 주지 않고 연결을 열어둔 채 기다린다.

데이터가 생겨 즉시 응답을 보내면, 클라이언트는 데이터를 처리하고 곧바로 다시 새로운 요청을 보내 기다린다. 이는 Polling보단 실시간성이 뛰어난 방식임은 맞으며 반복적으로 요청을 보내는 횟수가 줄어들기 때문에 네트워크 부하를 줄일 수 있다.

그렇지만 서버에 많은 수의 클라이언트 요청이 묶일 수 있어 서버 가용 커넥션 관리 부담이 커진다.

반면 웹소켓은 최초 연결 시에만 HTTP를 사용해 핸드셰이크 과정을 거치고, 이후에는 매우 작은 크기의 프레임 단위로 데이터를 전송하므로 네트워크 부하가 극적으로 줄어든다.

💡 HTTP는 TEXT 기반 메시지 전체를 하나의 덩어리로 처리하지만, 웹소켓처럼 프레임 구조를 사용하면 전체 메시지가 다 도착할 때까지 기다릴 필요 없이 조각 단위로 처리할 수 있고, 제어 프레임(Ping/Pong)을 통해 연결 상태를 효율적으로 체크할 수 있다.

하지만 모든 실시간 기능에 웹소켓도 정답은 아니다. 웹소켓은 항상 서버와 연결된 상태를 유지해야 하는데, 동시 접속자 수가 많아지면 서버 사이드 메모리 부하가 오고 커넥션 관리 비용이 증가한다. (→ BufferTCP File Descriptor) 또한 서버가 다운되거나 재배포시 어떻게 기존 연결을 복구 + 수평 확장시 클라이언트 간 메시지 동기화는 어떻게 할지 고려해야 한다.

👉 또한 초기 연결 시 발생하는 Handshake 오버헤드 + HTTP 헤더를 주고받아야 해서 다소 무겁다. 그렇지만 빈번한 데이터 교환이 일어날수록 웹소켓 효율성이 더 좋다. 여기서 오가는 데이터는 2~14바이트 수준의 프레임 단위기 때문에 오버헤드는 급감한다.

만약 서버에서 클라이언트로 일방적인 데이터 푸시만 필요하고, 클라이언트에서 서버로 보낼 메시지가 많지 않다면 SSE(Server Sent Events)가 훨씬 가벼울 것이다.

SSE는 응답의 Content-Typetext/event-stream으로 설정되어 있으며, 서버는 연결을 종료하지 않고, 데이터가 발생할 때마다 지속적으로 조각(Event)를 보낸다(Streaming). 클라이언트가 한 번 요청을 보내면 서버가 연결을 끊기 전까지 계속해서 데이터를 수신 대기한다.

SSE는 웹소켓보다 설계 효율성 측면에서 장점을 갖는데, (1) 별도의 프로토콜 업그레이드가 필요 없어 기존의 방화벽, 프록시, 로드 밸런서와 충돌이 거의 없으며 구현도 단순하다. (2) 브라우저가 스스로 재연결을 시도할 수 있다. 마지막으로 받은 이벤트 ID(Last-Event-ID)를 함께 보내기 때문에 유실된 데이터부터 다시 전송할 수 있다. (3) 일방향 스트리밍에 특화되어 메모리와 CPU 소비가 웹소켓보다 상대적으로 적다.

👉 예를 들어, 알림이나 실시간 대시보드 같은 서비스

채팅, 멀티플레이 게임, 협업 편집 도구 같이 밀리초 단위의 지연 시간이 중요하고 양방향 상호작용이 빈번한 환경이라면 웹소켓이 적합하다.

🤔 현재 프로젝트에 실시간 기능이 중요한지, 초당 메시지 발생량이나 예상 동시 접속자 수 등은 몇이나 되는지 면밀히 파악하고 도입해야 겠다.

비교 항목 Polling Long Polling SSE (Server-Sent Events) WebSocket
통신 프로토콜 HTTP/1.1, HTTP/2 HTTP/1.1, HTTP/2 HTTP/1.1, HTTP/2 WS / WSS (TCP 기반 독립 프로토콜)
통신 방향 단방향 (Client → Server) 단방향 (Client → Server) 단방향 (Server → Client) 양방향 (Full-duplex)
연결 메커니즘 Connectionless (매번 연결) Connectionless (응답 전까지 유지) Persistent (지속 연결) Persistent (상태 유지 연결)
지연 시간 (Latency) 높음 (요청 주기만큼 지연) 낮음 (데이터 발생 시 응답) 매우 낮음 극도로 낮음 (밀리초 단위)
네트워크 오버헤드 매우 높음 (반복적 Header 전송) 높음 (빈번한 재연결 및 Header) 낮음 (경량 텍스트 스트리밍) 매우 낮음 (초기 Handshake 이후 최소화)
재연결 제어 클라이언트가 주기적 요청 응답 수신 후 클라이언트가 재요청 브라우저 내장 자동 재연결 지원 어플리케이션 레벨에서 직접 구현
서버 자원 소모 CPU/Network 부하가 높음 커넥션 대기에 따른 스레드 점유 낮은 메모리 점유, 유지 비용 저렴 커넥션 수에 비례한 메모리/상태 관리
주요 활용 사례 단순 주기적 데이터 갱신 알림 센터, 실시간성 낮은 채팅 AI 스트리밍, 시세 전광판, 뉴스 피드 실시간 게임, 거래소, 화상 회의, 협업 툴

REFERENCES


  1. TLS Handshake ↩︎

  2. 웹소켓 연결로 업그레이드하기 ↩︎

  3. 프로토콜 업그레이드 메커니즘 ↩︎

  4. 101 Switching Protocols ↩︎

  5. WebSocket ↩︎