WebSocket
WebSocket은 하나의 TCP 연결 위에서 클라이언트와 서버가 양방향으로 실시간 통신할 수 있는 프로토콜입니다. HTTP Upgrade 핸드셰이크로 연결을 수립한 뒤, 이후부터는 HTTP 오버헤드 없이 프레임 단위로 데이터를 주고받습니다.
핵심 개념
HTTP vs WebSocket
HTTP는 요청-응답 모델로, 서버가 먼저 데이터를 보낼 수 없습니다. 실시간 데이터가 필요하면 클라이언트가 계속 폴링해야 합니다. WebSocket은 연결 수립 후 양방향 채널이 열려 서버도 언제든 데이터를 푸시할 수 있습니다. 프레임 헤더가 작아 효율적입니다.
핵심 개념
HTTP Upgrade 핸드셰이크
WebSocket 연결은 HTTP GET 요청으로 시작합니다. 클라이언트가 Upgrade: websocket, Connection: Upgrade 헤더와 Sec-WebSocket-Key를 전송합니다. 서버가 101 Switching Protocols로 응답하면 프로토콜이 전환됩니다. 이후 HTTP는 사용하지 않습니다.
핵심 개념
WebSocket 프레임
WebSocket 데이터는 프레임 단위로 전송됩니다. 프레임 헤더는 서버→클라이언트 2~10바이트, 클라이언트→서버 마스킹 키가 더해져 6~14바이트로 작습니다. OpCode로 Text(0x1), Binary(0x2), Ping(0x9), Pong(0xA), Close(0x8) 등을 구분합니다. 클라이언트→서버 메시지는 반드시 마스킹해야 합니다.
핵심 개념
Ping / Pong
연결 유지(Keep-alive) 메커니즘입니다. 한쪽이 Ping 프레임을 보내면 상대는 Pong으로 응답합니다. 네트워크 장애나 상대의 비정상 종료를 감지하는 데 쓰입니다. Pong 응답이 없을 때 연결을 닫을지는 구현이 정합니다.
핵심 개념
서브프로토콜과 확장
Sec-WebSocket-Protocol 헤더로 서브프로토콜을 협상합니다. STOMP(메시지 브로커), MQTT(IoT) 같은 프로토콜이 WebSocket 위에서 동작할 수 있습니다. permessage-deflate 확장으로 메시지를 압축해 대역폭을 절약할 수 있습니다.
핵심 개념
SSE vs WebSocket
SSE(Server-Sent Events)는 서버→클라이언트 단방향 스트림으로, HTTP 위에서 동작합니다. 자동 재연결, EventSource API 등 간단한 장점이 있습니다. 단방향 알림(주식 시세, 뉴스 피드)에는 SSE가, 채팅·게임 같은 양방향 통신에는 WebSocket이 적합합니다.
HTTP만으로는 왜 부족했나
HTTP는 클라이언트가 요청해야 서버가 응답하는 구조라서, 서버가 먼저 소식을 전하려면 클라이언트가 계속 물어보는 폴링에 기대야 했습니다. WebSocket은 HTTP 요청으로 시작한 연결을 양방향 메시지 채널로 바꿔 이 문제를 줄입니다. 프로토콜은 2011년 RFC 6455로 표준화되었고, 브라우저의 JavaScript API는 WHATWG HTML 표준이 정의합니다.
숫자로 확인하기
Sec-WebSocket-Key 값 뒤에 RFC 6455가 정한 고정 문자열(GUID)을 붙여 SHA-1으로 해시하고 Base64로 인코딩해 Sec-WebSocket-Accept로 돌려줍니다. RFC의 예시 키 dGhlIHNhbXBsZSBub25jZQ==는 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=가 됩니다.
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
WebSocket 연결은 어떻게 맺어지나요?
클라이언트가 Upgrade: websocket 헤더와 Sec-WebSocket-Key를 담은 HTTP GET을 보내고, 서버가 101 Switching Protocols와 Sec-WebSocket-Accept로 응답하면 같은 TCP 연결이 WebSocket 프레임을 주고받는 채널로 바뀝니다.
면접 질문
폴링, 롱폴링, SSE, WebSocket은 어떻게 고르나요?
서버에서 클라이언트로만 알리면 되고 HTTP 위에서 단순하게 가고 싶으면 SSE, 클라이언트와 서버가 자주 양방향으로 주고받으면 WebSocket을 씁니다. 갱신이 드물고 단순하면 폴링도 충분합니다.
면접 질문
서버가 여러 대일 때 메시지를 모든 사용자에게 어떻게 보내나요?
사용자 연결이 서로 다른 서버에 붙어 있으므로 서버끼리 메시지를 전달할 통로가 필요합니다. 외부 메시지 브로커(RabbitMQ, ActiveMQ 등)나 Pub/Sub 방식으로 각 서버에 메시지를 퍼뜨리고, 각 서버가 자기 연결에 전달합니다.
면접 질문
연결이 끊긴 것을 어떻게 알아내나요?
상대가 정상 종료하면 Close 프레임이 오지만 비정상 종료나 네트워크 단절은 알려지지 않을 수 있습니다. Ping/Pong 프레임이나 애플리케이션 하트비트를 주기적으로 주고받아 응답이 없으면 끊긴 것으로 처리합니다.
실제 시스템에서는
setAllowedOrigins로 지정하고 SockJS는 WebSocket을 쓸 수 없을 때의 대체 전송을 제공한다고 안내합니다.
Upgrade와 Connection 헤더를 직접 전달해야 하고, 1.29.6 이하에서는 proxy_http_version 1.1도 명시해야 한다고 설명합니다(1.29.7부터 기본값이 1.1). 프록시된 서버가 60초 동안 데이터를 보내지 않으면 연결이 닫히며 proxy_read_timeout으로 조절합니다.
Sec-WebSocket-Protocol로 서브프로토콜을 협상하고, 이름은 IANA 레지스트리에 등록됩니다. 압축은 RFC 7692의 permessage-deflate 확장이 정의합니다.
함께 보면 좋은 토픽
참고 자료
Java — Spring WebSocket + STOMP 서버 구현
import org.springframework.web.socket.*; import org.springframework.messaging.handler.annotation.*; import org.springframework.messaging.simp.SimpMessagingTemplate; /* ── WebSocket 설정 ──────────────────────────────────────── */ @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 인메모리 메시지 브로커 — /topic(브로드캐스트), /queue(1:1) registry.enableSimpleBroker("/topic", "/queue"); // 클라이언트 → 서버 메시지 prefix registry.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") .setAllowedOriginPatterns("*") .withSockJS(); // WebSocket 미지원 환경 폴백 } } /* ── 채팅 컨트롤러 ──────────────────────────────────────── */ @Controller public class ChatController { private final SimpMessagingTemplate messagingTemplate; public ChatController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } // 클라이언트 → /app/chat.send → 브로드캐스트 @MessageMapping("/chat.send") @SendTo("/topic/chat") public ChatMessage handleMessage(ChatMessage message) { message.setTimestamp(Instant.now()); return message; // /topic/chat 구독자 전체에 전달 } // 서버에서 특정 사용자에게 1:1 메시지 전송 public void sendToUser(String userId, String message) { messagingTemplate.convertAndSendToUser( userId, "/queue/notification", new Notification(message) ); } // 연결/해제 이벤트 처리 @EventListener public void handleConnect(SessionConnectedEvent event) { log.info("WebSocket 연결: {}", event.getUser()); } @EventListener public void handleDisconnect(SessionDisconnectEvent event) { log.info("WebSocket 해제: {}", event.getSessionId()); } }
@EnableWebSocketMessageBroker로 STOMP 메시지 브로커를 활성화합니다.
enableSimpleBroker는 인메모리 브로커로, 운영 환경에서는 Redis를 브로커로 사용해 여러 서버 인스턴스 간 메시지를 공유합니다.
@MessageMapping은 /app/chat.send로 오는 메시지를 처리하고, @SendTo는 /topic/chat 구독자 전체에 결과를 브로드캐스트합니다.
withSockJS()는 WebSocket을 지원하지 않는 환경에서 롱폴링 등으로 자동 폴백합니다.
현대 브라우저에서는 대부분 불필요하지만 기업 네트워크 프록시 환경에서 유용합니다.
setAllowedOriginPatterns("*")는 개발 환경용입니다. 운영 환경에서는 실제 도메인만 허용해야 합니다.
JavaScript — WebSocket 클라이언트 + 재연결 로직
// ── 기본 WebSocket API ─────────────────────────────────── const ws = new WebSocket('wss://example.com/ws'); ws.onopen = () => { console.log('연결 수립', ws.readyState); // OPEN = 1 ws.send('안녕하세요!'); // 텍스트 전송 ws.send(new Uint8Array([1,2,3])); // 바이너리 전송 }; ws.onmessage = (event) => { if (typeof event.data === 'string') { const msg = JSON.parse(event.data); console.log('수신:', msg); } }; ws.onclose = (event) => { // event.code: 1000(정상), 1006(비정상), 1011(서버오류) console.log(`종료: ${event.code} ${event.reason}`); }; ws.onerror = (error) => console.error('오류:', error); // ── 자동 재연결 래퍼 ───────────────────────────────────── class ReconnectingWebSocket { constructor(url, options = {}) { this.url = url; this.maxRetries = options.maxRetries || 5; this.retryDelay = options.retryDelay || 1000; this.retries = 0; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { this.retries = 0; // 성공 시 카운터 리셋 this.onopen?.(); }; this.ws.onmessage = (e) => this.onmessage?.(e); this.ws.onclose = (e) => { if (e.code !== 1000 && this.retries < this.maxRetries) { const delay = this.retryDelay * Math.pow(2, this.retries); setTimeout(() => this.connect(), delay); // 지수 백오프 this.retries++; } }; } send(data) { if (this.ws.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(data)); } }
WebSocket.readyState는 CONNECTING(0), OPEN(1), CLOSING(2), CLOSED(3) 네 가지 상태를 가집니다.
send() 호출 전 OPEN 상태인지 확인해야 합니다.
ws://는 평문, wss://는 TLS 암호화 WebSocket입니다. 운영 환경에서는 반드시 wss://를 사용합니다.
자동 재연결 시 지수 백오프(exponential backoff)를 적용합니다.
재연결 실패 시 점점 늘어나는 대기 시간으로 서버 부하를 줄입니다.
close 코드 1000은 정상 종료이며 재연결하지 않습니다. 1006은 비정상 종료(네트워크 단절 등)로 재연결을 시도합니다.
Q1
WebSocket 연결이 시작되는 방식은?
Q2
WebSocket 연결이 시작되는 방식은?
Q3
WebSocket이 일반 HTTP 요청/응답 방식과 다른 점은?
Q4
WebSocket이 일반 HTTP 요청/응답 방식과 다른 점은?
Q5
WebSocket이 실시간 서비스(채팅, 알림 등)에 적합한 이유는?
Q6
WebSocket이 실시간 서비스(채팅, 알림 등)에 적합한 이유는?