웹소켓(WebSocket)

Uhae·약 14시간 전

채팅, 알림, 실시간 시세는 어떻게 "새로고침 없이" 화면이 바뀔까요?
이 글은 웹소켓을 처음 접하는 분부터 운영 환경의 확장성과 보안까지 고민하는 분까지 단계별로 읽을 수 있게 구성했습니다.

목차

읽는 방법: 처음이라면 Part 1~3, 실무 경험이 있다면 Part 4~5부터 읽어도 됩니다.


Part 1. 입문: 웹소켓이 뭐예요?

1-1. HTTP의 한계

우리가 평소 쓰는 웹(HTTP)은 "요청해야 응답하는" 구조입니다.

순서클라이언트서버
1"새 메시지 있어요?"
2"없어요"
3"새 메시지 있어요?"
4"있어요! (안녕)"

항상 클라이언트가 먼저 물어봐야 서버가 대답할 수 있습니다.

서버가 먼저 "새 메시지 왔어요!" 하고 말을 걸 수 없습니다. 식당에 비유하면, 손님이 벨을 누르기 전에는 직원이 절대 먼저 오지 않는 식당입니다.

1-2. 웹소켓의 아이디어

웹소켓은 한 번 연결을 맺고 그 연결을 계속 유지하면서, 양쪽 모두 언제든 메시지를 보낼 수 있게 한 프로토콜입니다.

순서클라이언트서버
1연결 요청 (핸드셰이크)
2연결 수락 (이후 연결 유지)
3"새 메시지 도착!" (요청 없이 먼저 전송)
4"저도 보낼게요"
5"또 새 메시지!"

연결이 열려 있는 동안에는 양쪽 모두 언제든 먼저 보낼 수 있습니다.

비유하면 전화 통화입니다. 한 번 연결되면 둘 중 누구든 말할 수 있고, 매번 다시 걸 필요가 없습니다. 반면 HTTP는 편지에 가깝습니다. 보내고 답장을 기다리고, 끝나면 연결도 끝납니다.

1-3. 한눈에 비교

구분HTTP웹소켓
연결요청마다 (또는 짧게)한 번 맺고 유지
방향클라이언트 → 서버 시작양방향, 누구든 시작
서버의 먼저 보내기불가가능
오버헤드요청마다 헤더연결 후엔 작은 프레임 헤더
어울리는 곳조회, CRUD, 일반 페이지채팅, 알림, 게임, 시세, 협업 도구

1-4. "그냥 계속 요청하면 되지 않나요?"

가능합니다. 이를 폴링(polling) 이라고 합니다. 1초마다 "새 메시지 있어?"라고 물으면 사용자에겐 거의 실시간처럼 보입니다. 다만 한계가 있습니다.

  • 새 메시지가 없어도 계속 요청하므로 낭비가 큽니다. 1만 명이면 초당 1만 요청입니다.
  • 폴링 주기만큼 지연이 생깁니다.
  • 주기를 줄이면 지연은 줄지만 서버 부담이 늘어납니다.
방식설명특징
폴링주기적으로 요청구현 쉬움, 낭비 큼
롱폴링새 데이터가 생길 때까지 서버가 응답을 보류낭비 감소, 연결 관리 복잡
SSEHTTP 연결을 유지하며 서버 → 클라 단방향 push단순, 텍스트 중심, 단방향
웹소켓양방향 상시 연결실시간성 최고, 연결 상태 관리 필요

Part 2. 기초: 동작 원리와 첫 코드

2-1. 연결이 맺어지는 과정 (핸드셰이크)

웹소켓은 HTTP로 시작해서 웹소켓으로 "업그레이드" 됩니다. 그래서 80/443 포트를 그대로 쓰고, 기존 웹 인프라와 잘 어울립니다.

클라이언트 요청

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

서버 응답

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

상태 코드 101 Switching Protocols 이후부터는 HTTP가 아니라 웹소켓 프레임으로 통신합니다.

  • URL 스킴은 ws://(평문), wss://(TLS 암호화)입니다. 운영에서는 반드시 wss:// 를 씁니다.

2-2. 브라우저에서 가장 단순한 예제

브라우저는 웹소켓 API를 기본 내장하고 있습니다.

const ws = new WebSocket("wss://example.com/chat");

ws.onopen = () => {
  console.log("연결됨");
  ws.send("안녕하세요");
};

ws.onmessage = (event) => {
  console.log("받은 메시지:", event.data);   // 요청 없이도 서버가 보내면 호출됨
};

ws.onclose = (event) => {
  console.log("연결 종료", event.code, event.reason);
};

ws.onerror = (err) => {
  console.error("에러", err);
};

핵심은 onmessage입니다. 서버가 보내는 순간 호출되므로 내가 따로 요청하지 않아도 다른 사람의 메시지를 받을 수 있습니다.

2-3. 연결 상태 (readyState)

값상수의미
0CONNECTING연결 중
1OPEN통신 가능
2CLOSING종료 중
3CLOSED종료됨

send()는 OPEN 상태에서만 안전합니다. 연결 전에 보내면 에러가 납니다.

2-4. 가장 흔한 오해 3가지

  1. "웹소켓은 HTTP를 대체한다": 아닙니다. 조회·로그인·CRUD는 여전히 REST가 더 적합하고, 보통 둘을 섞어 씁니다.
  2. "연결했으니 메시지가 절대 안 사라진다": 아닙니다. 연결이 끊긴 동안의 메시지는 받지 못합니다. 그래서 메시지는 DB에 저장하고 재접속 시 보충합니다.
  3. "웹소켓이면 무조건 빠르다": 연결 유지 비용(메모리, 소켓)이 듭니다. 실시간이 필요 없으면 오히려 낭비입니다.

Part 3. 실전: Spring으로 채팅 만들기

예제는 Spring Boot 3.x(Jakarta 기반) 기준입니다.

3-1. 의존성

implementation 'org.springframework.boot:spring-boot-starter-websocket'

Spring에서 웹소켓을 쓰는 방법은 크게 두 가지입니다.

방식특징
순수 WebSocket (WebSocketHandler)날것의 메시지를 직접 처리. 세션 관리·라우팅을 직접 구현
STOMP over WebSocket메시징 규약(구독/발행)을 얹어서 @MessageMapping 등으로 편하게 개발

3-2. 방식 A: 순수 WebSocket

@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {

    private final ChatHandler chatHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler, "/ws/chat")
                .setAllowedOriginPatterns("https://my-site.com"); // 운영에선 * 지양
    }
}
@Component
public class ChatHandler extends TextWebSocketHandler {

    // 동시성 문제를 피하려고 thread-safe 컬렉션 사용
    private final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        sessions.add(session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
        for (WebSocketSession s : sessions) {
            if (s.isOpen()) {
                s.sendMessage(message);   // 접속한 모두에게 브로드캐스트
            }
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        sessions.remove(session);
    }
}

이 방식에서는 "누가 어느 방에 있는지"를 직접 관리해야 합니다. 방이 많아지면 코드가 빠르게 복잡해집니다.

주의: WebSocketSession.sendMessage()는 같은 세션에 동시에 여러 스레드가 호출하면 안 됩니다. 필요하면 ConcurrentWebSocketSessionDecorator로 감싸세요.

3-3. 방식 B: STOMP (권장)

STOMP는 웹소켓 위에서 쓰는 간단한 텍스트 기반 메시징 규약입니다. 핵심 개념은 pub/sub입니다.

  • 구독(SUBSCRIBE): /topic/room.1 같은 목적지(destination)를 구독
  • 발행(SEND): 메시지를 서버로 보내면 서버가 구독자에게 전달
순서주체동작
1클라이언트 B/topic/room.1 구독 (SUBSCRIBE)
2클라이언트 C/topic/room.1 구독 (SUBSCRIBE)
3클라이언트 A/app/chat.send로 메시지 전송 (SEND)
4서버@MessageMapping("/chat.send") 메서드 실행, convertAndSend로 /topic/room.1에 발행
5브로커/topic/room.1을 구독 중인 모두(B, C, 그리고 구독 중이면 A)에게 MESSAGE 전달

설정

@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("https://my-site.com")
                .withSockJS();                       // 필요 시 폴백
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app"); // 클라 → 서버
        registry.enableSimpleBroker("/topic", "/queue");    // 서버 → 클라 (인메모리 브로커)
    }
}

컨트롤러

public record ChatMessage(String roomId, String sender, String content) {}

@Controller
@RequiredArgsConstructor
public class ChatController {

    private final SimpMessagingTemplate messagingTemplate;

    // 클라이언트가 /app/chat.send 로 보내면 호출됨
    @MessageMapping("/chat.send")
    public void send(ChatMessage message) {
        // 1) DB 저장 (재접속 시 내역 조회용)
        // 2) 해당 방 구독자에게 전달
        messagingTemplate.convertAndSend("/topic/room." + message.roomId(), message);
    }
}

@SendTo("/topic/...")를 쓰면 반환값을 자동으로 브로드캐스트할 수도 있지만, 방 ID처럼 동적 목적지는 SimpMessagingTemplate이 더 편합니다.

클라이언트 (stompjs)

import { Client } from "@stomp/stompjs";

const client = new Client({
  brokerURL: "wss://example.com/ws",
  reconnectDelay: 5000,                // 끊기면 5초 후 재연결
  heartbeatIncoming: 10000,
  heartbeatOutgoing: 10000,
  onConnect: () => {
    // 한 번만 구독하면, 이후 메시지는 자동으로 도착
    client.subscribe("/topic/room.1", (frame) => {
      const msg = JSON.parse(frame.body);
      render(msg);
    });
  },
});
client.activate();

function send(text) {
  client.publish({
    destination: "/app/chat.send",
    body: JSON.stringify({ roomId: "1", sender: "chichi", content: text }),
  });
}

withSockJS()를 쓰면 클라이언트도 SockJS 방식으로 연결해야 합니다(webSocketFactory). 요즘은 대부분의 환경이 웹소켓을 지원하므로 순수 brokerURL 방식도 많이 씁니다.

3-4. 채팅 시스템의 현실적인 구조

웹소켓만으로 채팅이 완성되지는 않습니다. 역할을 나누는 것이 일반적입니다.

기능수단
로그인, 방 생성, 방 목록REST API
이전 대화 내역 조회 (페이징)REST API
실시간 메시지 송수신WebSocket (STOMP)
메시지 영구 저장DB
오프라인 사용자 알림푸시 알림 등

접속하면 먼저 REST로 최근 내역을 불러오고, 그 뒤부터 웹소켓으로 새 메시지를 이어서 받는 흐름이 기본입니다.


Part 4. 중급: 운영에서 부딪히는 문제들

4-1. 인증과 인가

브라우저 WebSocket API는 커스텀 헤더(Authorization)를 직접 설정할 수 없습니다. 그래서 보통 다음 중 하나를 씁니다.

방법설명주의점
쿠키/세션핸드셰이크 시 쿠키가 자동 전송됨CSWSH(아래 참고) 방어 필요
쿼리스트링 토큰wss://.../ws?token=...URL이 로그에 남을 수 있음. 단기 토큰 권장
STOMP CONNECT 헤더연결 후 STOMP 프레임 헤더에 토큰서버에서 ChannelInterceptor로 검증
티켓 방식REST로 1회용 티켓 발급 → 웹소켓 연결 시 제출가장 안전하고 흔한 패턴

STOMP에서 인증하는 예시

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.interceptors(new ChannelInterceptor() {
        @Override
        public Message<?> preSend(Message<?> message, MessageChannel channel) {
            StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
            if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                String token = accessor.getFirstNativeHeader("Authorization");
                Principal user = authenticate(token);       // 검증 후 Principal 생성
                accessor.setUser(user);
            }
            return message;
        }
    });
}

그리고 구독 권한도 검사해야 합니다. 로그인했다는 것만으로 /topic/room.999를 마음대로 구독하게 두면 남의 방 메시지를 훔쳐볼 수 있습니다. SUBSCRIBE 명령 시점에도 "이 사용자가 이 방의 멤버인가?"를 확인하세요.

4-2. 보안 체크리스트

  • wss:// 사용: 평문 ws://는 도청·변조 위험이 있습니다.
  • Origin 검증 (CSWSH 방어): 웹소켓은 동일 출처 정책(SOP)이 적용되지 않습니다. 쿠키 인증을 쓰면 악성 사이트가 사용자 브라우저로 내 서버에 웹소켓을 열 수 있습니다(Cross-Site WebSocket Hijacking). setAllowedOriginPatterns를 명시적 도메인으로 제한하세요.
  • 입력 검증: 웹소켓 메시지도 결국 사용자 입력입니다. 길이·형식을 검증하고, 화면에 그릴 때는 XSS 방지를 위해 이스케이프합니다.
  • 메시지 크기 제한: 거대한 메시지로 메모리를 소진시키는 공격을 막습니다.
  • 연결 수 / 전송 빈도 제한: 사용자당 동시 연결 수와 초당 메시지 수에 상한을 둡니다(rate limiting).

4-3. 연결은 반드시 끊긴다: 하트비트와 재연결

모바일 네트워크 전환, 절전, 로드밸런서·프록시의 idle timeout(예: 60초) 등으로 연결은 조용히 죽습니다. 한쪽은 끊긴 줄 모르는 반쯤 열린(half-open) 연결도 생깁니다.

  • 하트비트(ping/pong): 주기적으로 신호를 주고받아 살아있는지 확인하고, 중간 장비가 idle로 끊지 않게 합니다.
    • 웹소켓 프로토콜 자체의 ping/pong 프레임이 있습니다.
    • STOMP에는 heartbeat 헤더가 따로 있습니다.
  • 재연결(reconnect): 클라이언트가 자동으로 다시 붙어야 합니다. 이때 지수 백오프(exponential backoff) + 지터(jitter) 를 쓰세요. 서버가 재시작되면 수만 클라이언트가 동시에 재접속해 서버를 다시 쓰러뜨리는 "thundering herd" 문제가 생깁니다.
  • 재접속 후 동기화: 끊긴 동안의 메시지는 못 받았으므로, "마지막으로 받은 메시지 ID 이후"를 REST로 조회해 채웁니다.

4-4. 메시지 순서와 유실, 중복

웹소켓(TCP)은 한 연결 안에서는 순서를 보장하지만, 애플리케이션 수준의 신뢰성은 보장하지 않습니다.

문제원인대응
유실끊긴 동안 발송된 메시지서버 저장 + 재접속 시 보충 조회
중복재전송, 재접속 보충과 실시간 수신 겹침메시지 고유 ID로 클라이언트에서 중복 제거
순서 꼬임다중 서버, 비동기 처리서버가 부여한 시퀀스/타임스탬프 기준 정렬
전송 확인보냈는지 불확실클라 → 서버 ACK, 클라이언트 임시 ID(낙관적 UI)

채팅에서 흔한 패턴은 클라이언트가 임시 ID(clientMsgId)를 붙여 보내고, 서버가 저장 후 정식 ID와 함께 되돌려주면 화면의 임시 메시지를 확정 상태로 교체하는 것입니다.

4-5. 메시지 포맷 설계

처음부터 타입이 있는 JSON 구조로 설계해 두면 확장이 쉽습니다.

{
  "type": "CHAT",
  "id": "01JABC...",
  "roomId": "1",
  "sender": "chichi",
  "content": "안녕",
  "sentAt": "2026-10-08T12:00:00Z"
}

type으로 CHAT, TYPING, READ, JOIN, LEAVE 등을 구분하면 입력 중 표시, 읽음 처리 같은 기능을 같은 연결 위에 얹을 수 있습니다. 트래픽이 아주 크면 JSON 대신 Protobuf, MessagePack 같은 바이너리 포맷도 고려합니다.

4-6. 테스트와 디버깅

  • 브라우저 개발자 도구 → Network → WS 탭에서 프레임을 직접 볼 수 있습니다.
  • CLI 도구: wscat, websocat
  • Spring: WebSocketStompClient 로 통합 테스트 작성 가능
npx wscat -c wss://example.com/chat

Part 5. 심화: 프로토콜 내부와 대규모 확장

5-1. 프레임 구조 (RFC 6455)

핸드셰이크 이후의 데이터는 프레임(frame) 단위입니다.

프레임은 앞에서부터 아래 필드가 순서대로 이어진 구조입니다.

순서필드크기설명
1FIN1비트이 프레임이 메시지의 마지막 조각인지 여부
2RSV1~3각 1비트확장용 예약 비트. 보통 0 (permessage-deflate는 RSV1 사용)
3opcode4비트프레임 종류 (아래 표 참고)
4MASK1비트payload 마스킹 여부. 클라이언트 → 서버는 항상 1
5Payload len7비트데이터 길이. 0~125면 이 값이 곧 길이, 126이면 확장 16비트, 127이면 확장 64비트 사용
6Extended payload length없음 / 16비트 / 64비트Payload len이 126 또는 127일 때만 존재
7Masking-key없음 / 4바이트MASK가 1일 때만 존재
8Payload Data가변실제 전송 데이터 (마스킹 키로 XOR 처리됨)
  • FIN: 마지막 조각인지 여부. 큰 메시지를 여러 프레임으로 나눌 수 있습니다(fragmentation).
  • opcode: 프레임 종류
opcode의미
0x0연속(continuation) 프레임
0x1텍스트(UTF-8)
0x2바이너리
0x8연결 종료(close)
0x9Ping
0xAPong
  • MASK: 클라이언트 → 서버 프레임은 반드시 마스킹해야 합니다. 서버 → 클라이언트는 마스킹하지 않습니다. 이는 암호화가 아니라, 오래된 프록시가 웹소켓 데이터를 HTTP로 오해해 캐시 오염을 일으키는 공격(cache poisoning)을 막기 위한 장치입니다.
  • Payload len: 7비트로 0~125, 126이면 뒤 16비트, 127이면 뒤 64비트가 실제 길이입니다. 그래서 헤더 오버헤드가 2~14바이트 수준으로 작습니다.

5-2. 핸드셰이크의 Sec-WebSocket-Accept

서버는 클라이언트가 보낸 Sec-WebSocket-Key에 고정 GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)를 붙여 SHA-1 해시 후 Base64 인코딩한 값을 돌려줍니다. 이는 "이 서버가 웹소켓 프로토콜을 이해하고 있다" 는 확인용이며, 인증이 아닙니다.

5-3. 종료 코드 (Close Code)

연결 종료 시 코드와 사유를 전달합니다. 디버깅할 때 매우 유용합니다.

코드의미
1000정상 종료
1001엔드포인트가 떠남 (서버 종료, 페이지 이동)
1006비정상 종료 (프레임 없이 끊김. 네트워크 단절 등, 실제로 전송되지 않는 예약 코드)
1008정책 위반
1009메시지가 너무 큼
1011서버 내부 오류

1006이 자주 보이면 프록시 타임아웃, 네트워크 문제, 서버 크래시를 의심하세요.

5-4. 서버 아키텍처: 왜 웹소켓은 확장이 어려운가

HTTP는 무상태라 서버를 늘리면 로드밸런서가 아무 서버로나 보내면 됩니다. 웹소켓은 연결 자체가 상태(state) 입니다.

사용자연결된 서버해당 서버가 가진 것
A서버 1A의 소켓
B서버 2B의 소켓

A가 B에게 메시지를 보내면 서버 1이 받는데, 서버 1에는 B의 소켓이 없어서 전달할 수 없습니다.

그래서 서버가 여러 대일 때는 서버 간 메시지를 중계하는 계층이 필요합니다.

해결책 1: 외부 브로커 (Pub/Sub)

각 서버가 Redis Pub/Sub, Kafka, RabbitMQ 같은 브로커를 구독하고, 메시지를 브로커에 발행하면 모든 서버가 받아 자신에게 연결된 사용자에게 전달합니다.

  1. 서버 1이 A에게 받은 메시지를 브로커(Redis, Kafka, RabbitMQ 등)에 발행합니다.
  2. 브로커가 구독 중인 모든 서버(서버 1, 서버 2)에 메시지를 전달합니다.
  3. 각 서버는 자기가 소켓을 들고 있는 사용자에게만 전송합니다. 여기서는 서버 2가 B에게 보냅니다.

Spring STOMP에서는 enableSimpleBroker 대신 enableStompBrokerRelay 로 RabbitMQ·ActiveMQ 등 STOMP 지원 브로커에 위임할 수 있습니다.

registry.enableStompBrokerRelay("/topic", "/queue")
        .setRelayHost("rabbitmq-host")
        .setRelayPort(61613)
        .setClientLogin("guest")
        .setClientPasscode("guest");

Redis Pub/Sub 방식은 구현이 간단하지만 구독자가 없으면 메시지가 사라지고(저장 안 됨) 전달 보장이 없다는 점을 알아야 합니다. 유실이 안 되어야 하면 Redis Streams, Kafka 등을 고려합니다.

해결책 2: 스티키 세션 / 라우팅

같은 사용자·같은 방을 같은 서버로 보내는 방식입니다. 단순하지만 부하 분산이 불균등하고 서버 장애 시 재배치가 어렵습니다. 보통 브로커 방식과 병행합니다.

5-5. 로드밸런서와 프록시 설정

  • 업그레이드 헤더 전달이 필요합니다. Nginx 예시:
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 3600s;   # 기본 60초면 idle 연결이 끊김
}
  • AWS ALB 등은 WebSocket을 지원하지만 idle timeout 기본값이 있으므로 하트비트 주기를 그보다 짧게 둡니다.
  • 배포 시 서버를 내리면 연결이 전부 끊깁니다. graceful shutdown(신규 연결 차단 → 클라에 재연결 유도 → 종료)과 클라 재연결 로직을 함께 설계하세요.

5-6. 동시 접속 수와 자원

  • 연결 하나는 파일 디스크립터 1개 + 메모리(버퍼, 세션 객체) 를 차지합니다. OS의 ulimit -n, 커널 소켓 설정을 늘려야 수만 연결을 감당합니다.
  • 스레드-당-연결 모델(전통적 서블릿 블로킹 I/O)은 연결이 많으면 부담이 큽니다. Netty 기반 / 논리적으로 비동기 I/O 서버(Spring WebFlux, Reactor Netty 등)가 대량 연결에 유리합니다. 최근에는 Java 21의 가상 스레드도 선택지입니다.
  • 흔히 한 대에서 수만~수십만 연결도 가능하지만, 실제 한계는 메시지 팬아웃(fan-out) 비용에서 먼저 옵니다. 1,000명이 있는 방에 초당 10개 메시지면 초당 1만 건 전송입니다.

5-7. 배압(Backpressure)과 느린 소비자

느린 클라이언트(느린 모바일 네트워크)에게 서버가 계속 보내면 서버 쪽 전송 버퍼가 쌓여 메모리가 터집니다.

  • 세션별 전송 큐 크기와 버퍼 한도를 제한하고, 초과 시 연결을 끊거나 오래된 메시지를 버립니다.
    • Spring: ConcurrentWebSocketSessionDecorator(session, sendTimeLimit, bufferSizeLimit)
    • WebSocketTransportRegistration의 setSendTimeLimit, setSendBufferSizeLimit
  • 시세처럼 "최신값만 중요한" 데이터는 중간값을 합치거나 버리는(conflation) 전략이 유효합니다.

5-8. 압축: permessage-deflate

확장(extension)으로 메시지 압축을 협상할 수 있습니다. 텍스트 위주 대용량 트래픽에 효과적이지만, CPU와 메모리를 더 쓰고, 압축과 암호화가 만나는 상황에서의 정보 누출(CRIME/BREACH 계열) 이슈도 고려해야 합니다. 소규모 메시지가 대부분이면 이득이 작을 수 있습니다.

5-9. 서브프로토콜

Sec-WebSocket-Protocol 헤더로 "이 연결 위에서 어떤 상위 규약을 쓸지"를 협상합니다. STOMP(v12.stomp), MQTT, GraphQL(graphql-transport-ws) 등이 대표적입니다. 즉 STOMP는 웹소켓과 별개의 규약이고, 웹소켓 위에 얹히는 것입니다.

5-10. HTTP/2, HTTP/3에서의 웹소켓

원래 웹소켓은 HTTP/1.1 업그레이드 방식입니다. HTTP/2 위에서는 RFC 8441(Extended CONNECT)로, HTTP/3는 RFC 9220으로 웹소켓을 한 연결에 멀티플렉싱할 수 있습니다. 다만 서버·프록시·브라우저 지원 상황이 달라, 실무에서는 아직 HTTP/1.1 업그레이드가 가장 널리 쓰입니다.

5-11. 웹소켓의 대안과 보완재

기술특징어울리는 곳
SSE서버 → 클라 단방향, 자동 재연결, HTTP 인프라 친화적알림, 피드, 진행률, LLM 스트리밍 응답
WebTransportHTTP/3(QUIC) 기반, 다중 스트림, 비신뢰성 데이터그램저지연 게임, 미디어 (아직 발전 중)
WebRTC DataChannel브라우저 간 P2P화상 통화, P2P 데이터
gRPC 스트리밍서버 간 양방향 스트림마이크로서비스 내부
MQTT (over WS)경량 pub/subIoT

"서버가 보내기만 하면 된다"면 SSE가 훨씬 단순한 경우가 많습니다. 웹소켓은 양방향이 정말 필요할 때 선택하세요.


Part 6. 정리: 언제 무엇을 쓸까

선택 가이드

우리 서비스의 상황추천 기술
서버가 먼저 보낼 필요가 없다 (조회, CRUD 위주)일반 HTTP (REST)
서버가 먼저 보내야 하지만, 서버에서 클라이언트로 가는 단방향이면 충분하다 (알림, 피드, 진행률)SSE
양방향 통신이 빈번하고, 방/주제 단위로 구독·발행이 필요하다 (채팅방, 협업 도구)WebSocket + STOMP
양방향 통신이 빈번하지만 구조가 단순하거나 직접 만든 규약을 쓴다순수 WebSocket

판단 순서는 위에서 아래로, "서버가 먼저 보내야 하나?" → "단방향이면 충분한가?" → "pub/sub 구조가 필요한가?" 입니다.

단계별 학습 로드맵

단계목표해볼 것
1개념 이해HTTP vs 폴링 vs 웹소켓 비교, 브라우저 콘솔에서 new WebSocket 실습
2간단한 채팅Spring 순수 WebSocket으로 전체 브로드캐스트
3구조화STOMP로 방 단위 구독/발행, DB 저장, 내역 조회 REST
4운영 대비인증·Origin 검증, 하트비트, 재연결, 중복/유실 처리
5확장Redis/RabbitMQ 브로커, 로드밸런서 설정, 부하 테스트

한 줄 요약

웹소켓 = 한 번 연결해 두고, 서버와 클라이언트가 언제든 서로에게 말을 거는 통로.
쓰기는 쉽지만, 운영에서는 끊김·재연결·인증·확장을 함께 설계해야 진짜 서비스가 됩니다.


참고 자료

0개의 댓글