WebSocket HTTP 통신 차이를 한 문장으로 요약하면 이렇습니다. “HTTP는 매번 문을 두드리고 답을 받은 뒤 문을 닫고, WebSocket은 한 번 문을 열면 닫을 때까지 양쪽이 자유롭게 오간다.” 카카오톡 메시지가 상대방 화면에 즉시 나타나고, 주식 호가가 1초마다 갱신되고, 온라인 게임에서 상대방 움직임이 실시간으로 보이는 것, 이 모든 경험의 뒤에는 HTTP가 아닌 WebSocket이 있습니다. 이 글에서는 두 프로토콜의 구조적 차이부터 실전 구현 코드, 그리고 언제 무엇을 선택해야 하는지까지 완전히 풀어봅니다.
목차
- HTTP 통신의 구조 – 요청-응답 사이클의 원리와 한계
- 실시간 통신의 필요와 HTTP 기반 임시 해결책들
- WebSocket의 탄생 – 핸드셰이크와 지속 연결 구조
- WebSocket vs HTTP 핵심 차이 – 5가지 관점 비교
- WebSocket 실전 구현 – 채팅 서버 파이썬·자바스크립트 코드
- 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.1 | HTTP/2 | WebSocket |
|---|---|---|---|
| 연결당 헤더 크기 | 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) | SSE | WebSocket |
|---|---|---|---|
| 통신 방향 | 단방향 (클→서) | 단방향 (서→클) | 양방향 |
| 연결 유지 | 요청마다 재수립 | 지속 연결 | 지속 연결 |
| 프로토콜 | HTTP/1.1, 2, 3 | HTTP | ws:// 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 중 가장 적합한 도구를 선택하는 것이 올바른 아키텍처 설계의 시작점입니다.
답글 남기기