WebSocket HTTP 통신 차이 – 실시간 통신은 왜 다른 기술이 필요한가


WebSocket HTTP 통신 차이를 한 문장으로 요약하면 이렇습니다. “HTTP는 매번 문을 두드리고 답을 받은 뒤 문을 닫고, WebSocket은 한 번 문을 열면 닫을 때까지 양쪽이 자유롭게 오간다.” 카카오톡 메시지가 상대방 화면에 즉시 나타나고, 주식 호가가 1초마다 갱신되고, 온라인 게임에서 상대방 움직임이 실시간으로 보이는 것, 이 모든 경험의 뒤에는 HTTP가 아닌 WebSocket이 있습니다. 이 글에서는 두 프로토콜의 구조적 차이부터 실전 구현 코드, 그리고 언제 무엇을 선택해야 하는지까지 완전히 풀어봅니다.


목차

  1. HTTP 통신의 구조 – 요청-응답 사이클의 원리와 한계
  2. 실시간 통신의 필요와 HTTP 기반 임시 해결책들
  3. WebSocket의 탄생 – 핸드셰이크와 지속 연결 구조
  4. WebSocket vs HTTP 핵심 차이 – 5가지 관점 비교
  5. WebSocket 실전 구현 – 채팅 서버 파이썬·자바스크립트 코드
  6. WebSocket·HTTP·SSE 선택 전략과 실무 아키텍처 설계

1. HTTP 통신의 구조 – 요청-응답 사이클의 원리와 한계

HTTP는 월드와이드웹의 근간 프로토콜입니다. 1991년 탄생 이후 30년 넘게 웹을 지탱해온 검증된 기술이지만, 설계 철학 자체가 실시간 통신에 적합하지 않습니다.

HTTP의 요청-응답 구조

[HTTP 통신 기본 구조]

클라이언트 (브라우저)              서버
     │                              │
     │──── 요청 (Request) ─────────→│
     │     GET /api/messages         │
     │     Host: example.com         │
     │                              │
     │←─── 응답 (Response) ─────────│
     │     200 OK                    │
     │     {"messages": [...]}       │
     │                              │
     ×  연결 종료 (기본 동작)        ×
     │                              │
     │  (5초 후 새 메시지 확인)       │
     │──── 요청 (Request) ─────────→│
     │     GET /api/messages         │
     │                              │
     │←─── 응답 (Response) ─────────│
     │                              │
     ×  연결 종료                    ×

문제: 서버가 먼저 말을 걸 수 없다
     "새 메시지가 왔어요!"를 클라이언트가
     주기적으로 물어봐야만 알 수 있음

HTTP의 세 가지 구조적 한계

① 단방향성 – 서버는 먼저 말할 수 없다

HTTP는 항상 클라이언트가 먼저 요청해야 서버가 응답할 수 있습니다. 서버가 클라이언트에게 먼저 데이터를 푸시하는 것이 구조적으로 불가능합니다. 채팅 앱에서 상대방이 메시지를 보내도, 내 브라우저가 서버에 “새 메시지 있나요?”라고 물어보기 전까지는 알 수 없습니다.

② 상태 비저장(Stateless) – 매 요청이 독립적

HTTP는 본질적으로 무상태(Stateless) 프로토콜입니다. 각 요청은 이전 요청과 완전히 독립적이며, 서버는 클라이언트를 기억하지 않습니다. 연속된 대화 맥락을 유지하려면 쿠키·세션·JWT 같은 별도 메커니즘이 필요합니다.

③ 헤더 오버헤드 – 매 요청마다 반복되는 메타데이터

[HTTP 헤더 오버헤드 예시]

실제 전송하고 싶은 데이터:
  "안녕하세요"  ← 15바이트

HTTP 요청에 포함되는 헤더:
  POST /chat HTTP/1.1                    ← 21바이트
  Host: chat.example.com                 ← 22바이트
  Content-Type: application/json         ← 30바이트
  Authorization: Bearer eyJhbGciOiJI...  ← 수백 바이트
  Cookie: session=abc123; pref=dark...   ← 수십~수백 바이트
  Accept: application/json               ← 26바이트
  Accept-Encoding: gzip, deflate, br     ← 35바이트
  User-Agent: Mozilla/5.0 ...            ← 100+ 바이트
  ──────────────────────────────────────
  총 헤더: 약 500~2,000 바이트
  실제 페이로드: 15바이트

→ 채팅 메시지 100개 전송 시 헤더만 50,000~200,000바이트 낭비
  실시간으로 수천 명이 동시 접속하면 헤더 오버헤드만으로 서버 부하 폭증

2. 실시간 통신의 필요와 HTTP 기반 임시 해결책들

WebSocket 이전에도 개발자들은 HTTP만으로 실시간에 가까운 통신을 구현하려 했습니다. 이 임시 해결책들의 한계가 WebSocket 탄생을 이끌었습니다.

해결책 1 – 폴링 (Polling)

[폴링 동작 방식]

클라이언트                              서버
    │                                    │
    │──── GET /messages ────────────────→│
    │←─── 200 OK {"messages": []} ───────│  (새 메시지 없음)
    │                                    │
    │  (3초 대기)                         │
    │                                    │
    │──── GET /messages ────────────────→│
    │←─── 200 OK {"messages": []} ───────│  (새 메시지 없음)
    │                                    │
    │  (3초 대기)                         │  ← 이 시간에 메시지 도착!
    │                                    │
    │──── GET /messages ────────────────→│
    │←─── 200 OK {"messages": ["안녕"]}──│  (3초 뒤에야 수신)

단점:
  - 메시지 지연 = 폴링 주기 (3초 설정 시 최대 3초 지연)
  - 새 메시지가 없어도 계속 요청 → 서버·네트워크 낭비
  - 동시 접속자 1만 명 × 초당 1회 폴링 = 초당 10,000건 빈 요청

해결책 2 – 롱 폴링 (Long Polling)

[롱 폴링 동작 방식]

클라이언트                              서버
    │                                    │
    │──── GET /messages ────────────────→│
    │                                    │  서버가 응답을 보류
    │     (대기 중...)                    │  (새 메시지 올 때까지 연결 유지)
    │                                    │
    │                                    │  ← 새 메시지 도착!
    │←─── 200 OK {"messages": ["안녕"]}──│  즉시 응답
    │                                    │
    │──── GET /messages ────────────────→│  즉시 다음 연결 시작
    │     (다시 대기 중...)               │

개선점: 폴링 대비 지연 감소, 빈 응답 횟수 감소
단점:
  - 서버가 연결을 오래 붙들고 있어야 함 (스레드·메모리 소모)
  - 메시지가 폭발적으로 증가하면 연결 재수립 오버헤드
  - 단방향(서버→클라이언트)만 효율적

해결책 3 – Server-Sent Events (SSE)

python

# SSE – 서버가 클라이언트에게 단방향 스트림 전송
# HTTP 연결을 유지하면서 서버가 이벤트를 밀어넣는 방식

from flask import Flask, Response
import time

app = Flask(__name__)

@app.route('/stream')
def stream():
    def event_generator():
        while True:
            # text/event-stream 형식으로 데이터 전송
            yield f"data: {{'time': '{time.time()}', 'price': '75000'}}\n\n"
            time.sleep(1)

    return Response(
        event_generator(),
        mimetype='text/event-stream',
        headers={
            'Cache-Control': 'no-cache',
            'X-Accel-Buffering': 'no'   # Nginx 버퍼링 비활성화
        }
    )

# 클라이언트 (JavaScript)
# const evtSource = new EventSource('/stream');
# evtSource.onmessage = (e) => console.log(JSON.parse(e.data));

# SSE 장점: HTTP 기반이라 기존 인프라 재사용, 자동 재연결
# SSE 단점: 단방향(서버→클라이언트)만 가능
#           클라이언트→서버는 별도 HTTP 요청 필요

세 가지 임시 해결책의 한계 비교

방식실시간성서버 부하양방향프로토콜
폴링낮음 (주기 의존)매우 높음HTTP
롱 폴링중간높음HTTP
SSE높음낮음❌ (단방향)HTTP
WebSocket매우 높음낮음✅ 양방향WS/WSS

3. WebSocket의 탄생 – 핸드셰이크와 지속 연결 구조

WebSocket은 2011년 RFC 6455로 표준화된 프로토콜입니다. HTTP의 한계를 해결하기 위해 HTTP 연결을 업그레이드하는 방식으로 설계되었으며, 한 번 연결되면 클라이언트와 서버가 자유롭게 양방향으로 데이터를 주고받습니다.

WebSocket 핸드셰이크 – HTTP에서 WS로 업그레이드

[WebSocket 연결 수립 과정]

Step 1: 클라이언트가 HTTP Upgrade 요청
────────────────────────────────────────────
GET /chat HTTP/1.1
Host: chat.example.com
Upgrade: websocket                ← 핵심: WebSocket으로 업그레이드 요청
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==  ← 랜덤 Base64 키
Sec-WebSocket-Version: 13

Step 2: 서버가 101 Switching Protocols 응답
────────────────────────────────────────────
HTTP/1.1 101 Switching Protocols  ← 프로토콜 전환 승인
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
                      ↑ 클라이언트 키를 SHA-1 해싱해 응답 (보안 검증)

Step 3: WebSocket 연결 수립 완료
────────────────────────────────────────────
  이후 HTTP 프로토콜 사용 종료
  동일 TCP 연결 위에서 WebSocket 프레임 통신 시작
  → ws:// 또는 wss:// (TLS 적용) 프로토콜로 전환

WebSocket 프레임 구조

[WebSocket 데이터 프레임 구조]

HTTP 헤더(500~2000바이트)와 달리 WebSocket 프레임 헤더는 2~14바이트

┌─────┬─────┬──────────┬──────────────┬─────────────┐
│ FIN │ RSV │ Opcode   │ Payload Len  │   Payload   │
│ 1bit│3bits│  4bits   │   7~71bits   │   (데이터)  │
└─────┴─────┴──────────┴──────────────┴─────────────┘

FIN    : 마지막 프레임 여부 (메시지 분할 전송 지원)
Opcode : 0x1=텍스트, 0x2=바이너리, 0x8=연결종료,
         0x9=Ping, 0xA=Pong
Payload: 실제 전송 데이터

→ "안녕하세요" (15바이트) 전송 시
  HTTP:      500~2,000바이트 헤더 + 15바이트
  WebSocket: 2~6바이트 프레임 헤더 + 15바이트
  → 오버헤드 99% 감소!

WebSocket 연결 생명주기

[WebSocket 전체 생명주기]

① 연결 수립 (HTTP Upgrade Handshake)
   HTTP → WebSocket 프로토콜 전환 (1회만 수행)
         ↓
② 데이터 교환 (양방향 프레임 송수신)
   클라이언트 → 서버: 메시지 전송
   서버 → 클라이언트: 메시지 전송
   (어느 쪽이든 먼저 보낼 수 있음, 동시 가능)
         ↓
③ Ping / Pong (연결 유지 확인)
   서버: Ping 프레임 전송 (연결 살아있나 확인)
   클라이언트: Pong 프레임 응답
   (기본 주기: 30~60초)
         ↓
④ 연결 종료 (Close Handshake)
   어느 쪽이든 Close 프레임 전송 가능
   상대방이 Close 응답 → TCP 연결 완전 종료
   상태 코드: 1000(정상), 1001(페이지 이탈),
              1006(비정상 종료) 등

4. WebSocket vs HTTP 핵심 차이 – 5가지 관점 비교

관점 1 – 연결 방식

[연결 방식 비교]

HTTP:
  클라 ──[요청]──→ 서버 ──[응답]──→ 클라 → 연결 종료
  클라 ──[요청]──→ 서버 ──[응답]──→ 클라 → 연결 종료
  클라 ──[요청]──→ 서버 ──[응답]──→ 클라 → 연결 종료
  (매번 TCP 연결 수립 → 데이터 전송 → 연결 해제 반복)
  * HTTP/1.1 Keep-Alive: TCP 재사용 가능하나 요청-응답 구조는 동일

WebSocket:
  클라 ──[HTTP Upgrade]──→ 서버
       ←────────────────── (101 Switching Protocols)
  ↑↑↑ 이후 지속 연결 ↑↑↑
  클라 ←──────────────────→ 서버  (양방향 자유 전송)
  클라 ←──────────────────→ 서버
  클라 ←──────────────────→ 서버
       ──[Close]──→
  연결 종료 (명시적 종료 전까지 유지)

관점 2 – 통신 방향

HTTP:  클라이언트 ──→ 서버  (단방향 요청)
                  ←── 서버  (요청에 대한 응답)
       → 서버가 먼저 말할 수 없음

WebSocket: 클라이언트 ←──→ 서버  (완전 양방향)
           → 양쪽 모두 언제든지 먼저 메시지 전송 가능
           → 동시에 송수신 가능 (Full-Duplex)

관점 3 – 오버헤드

항목HTTP/1.1HTTP/2WebSocket
연결당 헤더 크기500~2,000바이트압축 후 수십 바이트2~14바이트
연결 재사용Keep-Alive (제한적)멀티플렉싱지속 연결
서버 푸시제한적 (HTTP/2 Push)✅ 자유로운 푸시
프로토콜 전환 비용없음없음최초 1회 핸드셰이크

관점 4 – 상태 관리

python

# HTTP – 무상태, 매 요청마다 신원 증명 필요
# 매 요청에 Authorization 헤더 포함

import requests

# 요청 1
response1 = requests.get(
    'https://api.example.com/messages',
    headers={'Authorization': 'Bearer eyJhbGci...'}  # 매번 토큰 필요
)

# 요청 2 (서버는 이전 요청을 기억하지 않음)
response2 = requests.post(
    'https://api.example.com/messages',
    headers={'Authorization': 'Bearer eyJhbGci...'},  # 또 토큰 필요
    json={'content': '안녕하세요'}
)

# ─────────────────────────────────────────────────────────

# WebSocket – 연결 수립 시 1회 인증 후 상태 유지
import asyncio
import websockets

async def chat_client():
    uri = "wss://chat.example.com/ws"

    # 연결 수립 시 토큰을 헤더에 1회 포함
    async with websockets.connect(
        uri,
        extra_headers={'Authorization': 'Bearer eyJhbGci...'}
    ) as ws:
        # 이후 메시지 전송 시 인증 정보 불필요
        # 서버는 이 연결이 누구인지 이미 알고 있음
        await ws.send('{"type": "message", "content": "안녕하세요"}')
        await ws.send('{"type": "message", "content": "반갑습니다"}')
        await ws.send('{"type": "message", "content": "잘 부탁드려요"}')
        # 3번 전송했지만 인증은 연결 수립 시 1회만

관점 5 – 사용 사례 적합성

[프로토콜 선택 기준]

HTTP가 적합한 경우:
  ✅ 데이터 조회 (REST API, GraphQL)
  ✅ 파일 업로드/다운로드
  ✅ 캐싱이 필요한 리소스 (이미지, HTML, CSS)
  ✅ 요청-응답 주기가 명확한 작업
  ✅ 검색, 페이지 이동, 폼 제출

WebSocket이 적합한 경우:
  ✅ 실시간 양방향 채팅
  ✅ 온라인 멀티플레이어 게임
  ✅ 실시간 주식·코인 호가 스트리밍
  ✅ 협업 도구 (Google Docs 동시 편집)
  ✅ IoT 센서 데이터 실시간 모니터링
  ✅ 라이브 스포츠 점수 업데이트

5. WebSocket 실전 구현 – 채팅 서버 파이썬·자바스크립트 코드

Python WebSocket 서버 (websockets 라이브러리)

python

import asyncio
import json
import logging
from datetime import datetime
from typing import Set
import websockets
from websockets.server import WebSocketServerProtocol

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# 연결된 클라이언트 관리 (방 개념 포함)
class ChatRoom:
    def __init__(self, room_id: str):
        self.room_id = room_id
        self.clients: Set[WebSocketServerProtocol] = set()
        self.message_history: list = []

    async def broadcast(self, message: dict, exclude: WebSocketServerProtocol = None):
        """연결된 모든 클라이언트에게 메시지 브로드캐스트"""
        if not self.clients:
            return

        payload = json.dumps(message, ensure_ascii=False)

        # 끊긴 연결 처리를 위한 안전한 브로드캐스트
        disconnected = set()
        for client in self.clients:
            if client == exclude:
                continue
            try:
                await client.send(payload)
            except websockets.exceptions.ConnectionClosed:
                disconnected.add(client)

        # 끊긴 클라이언트 정리
        self.clients -= disconnected


class ChatServer:
    def __init__(self):
        self.rooms: dict[str, ChatRoom] = {}

    def get_or_create_room(self, room_id: str) -> ChatRoom:
        if room_id not in self.rooms:
            self.rooms[room_id] = ChatRoom(room_id)
        return self.rooms[room_id]

    async def handle_connection(
        self,
        websocket: WebSocketServerProtocol,
        path: str
    ):
        """
        WebSocket 연결 핸들러
        path 예시: /chat/room-1?username=홍길동
        """
        # 경로에서 방 ID 파싱
        room_id = path.split('/chat/')[-1].split('?')[0] or 'general'
        username = '익명'

        # 쿼리 파라미터에서 사용자명 추출
        if '?' in path:
            params = dict(p.split('=') for p in path.split('?')[1].split('&'))
            username = params.get('username', '익명')

        room = self.get_or_create_room(room_id)
        room.clients.add(websocket)

        logger.info(f"[{room_id}] '{username}' 입장 (총 {len(room.clients)}명)")

        # 입장 알림 브로드캐스트
        await room.broadcast({
            'type':      'system',
            'message':   f'{username}님이 입장했습니다.',
            'userCount': len(room.clients),
            'timestamp': datetime.now().isoformat()
        }, exclude=websocket)

        # 최근 메시지 히스토리 전송 (신규 입장자에게)
        if room.message_history:
            await websocket.send(json.dumps({
                'type':    'history',
                'messages': room.message_history[-20:]  # 최근 20개
            }, ensure_ascii=False))

        try:
            # 메시지 수신 루프 (연결 유지)
            async for raw_message in websocket:
                try:
                    data = json.loads(raw_message)
                    msg_type = data.get('type', 'message')

                    if msg_type == 'message':
                        # 채팅 메시지 처리
                        chat_msg = {
                            'type':      'message',
                            'username':  username,
                            'content':   data.get('content', ''),
                            'timestamp': datetime.now().isoformat()
                        }
                        room.message_history.append(chat_msg)
                        # 보낸 사람 포함 전체 브로드캐스트
                        await room.broadcast(chat_msg)

                    elif msg_type == 'typing':
                        # 타이핑 중 상태 알림 (보낸 사람 제외)
                        await room.broadcast({
                            'type':     'typing',
                            'username': username,
                            'isTyping': data.get('isTyping', False)
                        }, exclude=websocket)

                except json.JSONDecodeError:
                    await websocket.send(json.dumps({
                        'type':  'error',
                        'message': '잘못된 메시지 형식입니다.'
                    }))

        except websockets.exceptions.ConnectionClosed as e:
            logger.info(f"[{room_id}] '{username}' 연결 종료: {e.code}")

        finally:
            # 퇴장 처리 (예외 발생해도 반드시 실행)
            room.clients.discard(websocket)
            await room.broadcast({
                'type':      'system',
                'message':   f'{username}님이 퇴장했습니다.',
                'userCount': len(room.clients),
                'timestamp': datetime.now().isoformat()
            })


async def main():
    server = ChatServer()
    async with websockets.serve(
        server.handle_connection,
        host='0.0.0.0',
        port=8765,
        ping_interval=30,   # 30초마다 Ping 전송 (연결 유지 확인)
        ping_timeout=10,    # 10초 내 Pong 없으면 연결 종료
        max_size=1_048_576  # 최대 메시지 크기 1MB
    ):
        logger.info("WebSocket 채팅 서버 시작: ws://0.0.0.0:8765")
        await asyncio.Future()  # 서버 무한 실행

if __name__ == '__main__':
    asyncio.run(main())

JavaScript 클라이언트 (브라우저)

javascript

// WebSocket 채팅 클라이언트 – 재연결·에러 처리 포함

class ChatClient {
  constructor(serverUrl, username, roomId = 'general') {
    this.serverUrl = serverUrl;
    this.username  = username;
    this.roomId    = roomId;
    this.ws        = null;
    this.reconnectAttempts = 0;
    this.maxReconnects     = 5;
    this.reconnectDelay    = 1000; // 1초 시작, 지수 백오프
  }

  connect() {
    const url = `${this.serverUrl}/chat/${this.roomId}?username=${encodeURIComponent(this.username)}`;
    this.ws = new WebSocket(url);

    // ── 이벤트 핸들러 등록 ──────────────────────────────────

    this.ws.onopen = (event) => {
      console.log('WebSocket 연결 성공');
      this.reconnectAttempts = 0; // 재연결 카운터 초기화
      this.onConnected?.();
    };

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);

      switch (data.type) {
        case 'message':
          // 일반 채팅 메시지 수신
          this.onMessage?.(data);
          this.renderMessage(data);
          break;

        case 'system':
          // 입장/퇴장 시스템 메시지
          this.renderSystemMessage(data.message, data.userCount);
          break;

        case 'typing':
          // 타이핑 중 상태 표시
          this.showTypingIndicator(data.username, data.isTyping);
          break;

        case 'history':
          // 이전 메시지 히스토리 렌더링
          data.messages.forEach(msg => this.renderMessage(msg));
          break;

        case 'error':
          console.error('서버 오류:', data.message);
          break;
      }
    };

    this.ws.onclose = (event) => {
      console.log(`연결 종료: 코드=${event.code}, 이유=${event.reason}`);

      // 정상 종료(1000)가 아니면 재연결 시도
      if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnects) {
        const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
        console.log(`${delay}ms 후 재연결 시도 (${this.reconnectAttempts + 1}/${this.maxReconnects})`);
        setTimeout(() => {
          this.reconnectAttempts++;
          this.connect(); // 재귀 재연결
        }, delay);
      }
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket 오류:', error);
    };
  }

  // ── 메시지 전송 메서드들 ───────────────────────────────────

  sendMessage(content) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({
        type:    'message',
        content: content
      }));
    } else {
      console.warn('WebSocket이 연결되지 않음. 현재 상태:', this.ws?.readyState);
    }
  }

  sendTyping(isTyping) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({
        type:     'typing',
        isTyping: isTyping
      }));
    }
  }

  disconnect() {
    // 정상 종료 – 재연결 방지를 위해 코드 1000 사용
    this.ws?.close(1000, '사용자가 채팅방을 나갔습니다.');
  }

  // ── UI 렌더링 헬퍼 ─────────────────────────────────────────

  renderMessage({ username, content, timestamp }) {
    const chatBox = document.getElementById('chat-box');
    const div = document.createElement('div');
    div.className = `message ${username === this.username ? 'mine' : 'others'}`;
    div.innerHTML = `
      <span class="username">${username}</span>
      <span class="content">${content}</span>
      <span class="time">${new Date(timestamp).toLocaleTimeString()}</span>
    `;
    chatBox.appendChild(div);
    chatBox.scrollTop = chatBox.scrollHeight; // 최신 메시지로 스크롤
  }

  renderSystemMessage(message, userCount) {
    const chatBox = document.getElementById('chat-box');
    const div = document.createElement('div');
    div.className = 'system-message';
    div.textContent = `${message} (현재 ${userCount}명)`;
    chatBox.appendChild(div);
  }

  showTypingIndicator(username, isTyping) {
    const indicator = document.getElementById('typing-indicator');
    indicator.textContent = isTyping ? `${username}님이 입력 중...` : '';
  }
}

// ── 사용 예시 ──────────────────────────────────────────────────
const client = new ChatClient(
  'wss://chat.example.com',
  '홍길동',
  'room-1'
);

client.connect();

// 메시지 전송 버튼
document.getElementById('send-btn').addEventListener('click', () => {
  const input = document.getElementById('message-input');
  if (input.value.trim()) {
    client.sendMessage(input.value);
    input.value = '';
  }
});

// 타이핑 감지
let typingTimer;
document.getElementById('message-input').addEventListener('input', () => {
  client.sendTyping(true);
  clearTimeout(typingTimer);
  typingTimer = setTimeout(() => client.sendTyping(false), 1000);
});

6. WebSocket·HTTP·SSE 선택 전략과 실무 아키텍처 설계

세 가지 통신 방식 최종 비교표

항목HTTP (REST)SSEWebSocket
통신 방향단방향 (클→서)단방향 (서→클)양방향
연결 유지요청마다 재수립지속 연결지속 연결
프로토콜HTTP/1.1, 2, 3HTTPws:// wss://
헤더 오버헤드높음중간매우 낮음
자동 재연결❌ (직접 구현)✅ 브라우저 내장❌ (직접 구현)
브라우저 지원모든 브라우저IE 미지원IE10+
방화벽 친화성✅ 매우 좋음✅ 좋음⚠️ 일부 차단 가능
구현 복잡도낮음낮음중간~높음
적합 사례API, 파일 전송알림, 피드채팅, 게임

통신 방식 선택 플로차트

[어떤 통신 방식을 선택할까?]

서버가 클라이언트에게 먼저 데이터를 보내야 하는가?
    │
    ├── NO → HTTP (REST/GraphQL) 사용
    │         일반 API, 데이터 조회, 파일 업로드
    │
    └── YES
          ↓
      클라이언트도 서버에게 실시간 데이터를 보내야 하는가?
          │
          ├── NO → SSE (Server-Sent Events) 사용
          │         알림, 뉴스피드, 진행 상태 스트리밍
          │         주식 차트 (보기만 함)
          │
          └── YES → WebSocket 사용
                      채팅, 게임, 협업 편집
                      실시간 경매, 라이브 퀴즈

실무 아키텍처 – 대규모 WebSocket 서비스 설계

[수만 명 동시 접속 WebSocket 서버 아키텍처]

클라이언트들
    ↓ wss://
┌───────────────────────────────────┐
│  로드 밸런서 (L4/L7)              │
│  (Sticky Session 또는 IP Hash     │
│   같은 클라이언트 → 같은 서버)    │
└──────┬────────────┬───────────────┘
       ↓            ↓
  WS Server 1   WS Server 2  ...N대
  (10k 연결)    (10k 연결)
       │            │
       └──────┬─────┘
              ↓
    ┌──────────────────┐
    │  Redis Pub/Sub   │  ← 서버 간 메시지 브로드캐스트
    │  (메시지 브로커)  │     Server 1의 클라이언트에게 보낼 메시지를
    └──────────────────┘     Server 2도 구독해 전달
              ↓
    ┌──────────────────┐
    │  메시지 DB        │  ← 메시지 영속화 (MongoDB, Cassandra)
    │  (비동기 저장)    │
    └──────────────────┘

핵심 고려사항:
  ① Sticky Session: 같은 사용자의 요청이 같은 서버로 가도록
  ② Redis Pub/Sub: 서버 간 메시지 공유 (브로드캐스트)
  ③ 연결 수 제한: 서버당 적절한 연결 수 유지 (메모리 고려)
  ④ 하트비트: Ping/Pong으로 좀비 연결 탐지·제거

Socket.IO – WebSocket 실무 표준 라이브러리

javascript

// Socket.IO: WebSocket + 폴백(Polling) 자동 처리
// 방화벽 차단, 구형 브라우저 자동 대응

// 서버 (Node.js)
const { Server } = require('socket.io');
const io = new Server(3000, {
  cors: { origin: '*' },
  // WebSocket 불가 시 자동으로 Long Polling으로 폴백
  transports: ['websocket', 'polling']
});

io.on('connection', (socket) => {
  console.log(`연결: ${socket.id}`);

  // 방(Room) 입장
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    // 같은 방의 모든 사람에게 브로드캐스트
    io.to(roomId).emit('user-joined', { userId: socket.id });
  });

  // 채팅 메시지 수신 및 브로드캐스트
  socket.on('chat-message', ({ roomId, content }) => {
    io.to(roomId).emit('chat-message', {
      userId:    socket.id,
      content:   content,
      timestamp: Date.now()
    });
  });

  socket.on('disconnect', () => {
    console.log(`연결 종료: ${socket.id}`);
  });
});

결론

WebSocket HTTP 통신 차이의 핵심은 연결의 철학에 있습니다. HTTP는 요청이 있을 때만 문을 여는 ‘우편함 모델’이고, WebSocket은 한 번 연결하면 계속 열려있는 ‘전화 통화 모델’입니다. 두 기술 모두 TCP 위에서 동작하지만, 설계 목적이 다릅니다. HTTP는 캐싱·무상태·범용 API에 최적화되어 있고, WebSocket은 저지연 양방향 실시간 통신에 최적화되어 있습니다. 서비스의 통신 패턴을 먼저 분석하고, 선택 플로차트에 따라 HTTP·SSE·WebSocket 중 가장 적합한 도구를 선택하는 것이 올바른 아키텍처 설계의 시작점입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다