[WebSocket] 기본 개념 정리

dobby·2026년 4월 21일

Let's git it BE

목록 보기
2/20

WebSocket이 뭔지

REST API는 클라이언트가 먼저 요청해야만 서버가 응답할 수 있다.
반면 WebSocket은 한 번 연결하면 서버도 클라이언트도 언제든지 먼저 메시지를 보낼 수 있다.

[REST API - 편지]
클라이언트가 편지 보냄 → 서버가 답장
끝. 연결 종료.
다시 필요하면 또 편지 보냄

[WebSocket - 전화]
한 번 연결하면
서버도 먼저 말할 수 있고
클라이언트도 먼저 말할 수 있음
끊기 전까지 계속 대화 가능

왜 WebSocket을 쓰는가

속도가 빠르기 때문이 아니다.

진짜 이유는 서버가 클라이언트한테 먼저 데이터를 보낼 수 있기 때문이다.

REST API를 0.1초마다 폴링해도 기술적으로는 실시간처럼 보일 수 있다.
하지만 이 방식은 두 가지 문제가 있다.

문제 1. 서버가 보낼 준비가 됐어도
        클라이언트가 요청하기 전까지 전달이 안 됨

문제 2. 4명이 0.1초마다 요청하면
        4명 × 10번/초 = 40번/초 불필요한 요청 발생

WebSocket은 서버가 이벤트가 생기는 순간 바로 쏴줄 수 있어서
불필요한 요청 없이 진짜 실시간이 가능하다.


연결 / 구독 / 발행

WebSocket의 핵심 개념 3가지를 유튜브에 비유하면 이렇다.

개념유튜브 비유설명
연결유튜브 앱 켜기앱을 켜야 구독도 하고 댓글도 달 수 있듯이, 연결해야 구독/발행 가능
구독채널 구독구독하면 새 영상 알림 오듯이, 채널 구독하면 서버 메시지 자동 수신
발행댓글 달기내가 채널에 뭔가를 보내는 행위

WebSocket은 그냥 던지는 것

REST API와 WebSocket의 가장 큰 차이는 응답 방식이다.

REST API → 등기우편
내가 보내면 수신 확인 도장 받음
요청하면 반드시 나한테 1:1 응답이 옴
200 OK or 400 Bad Request

WebSocket → 단체 카톡방에 메시지 보내기
내가 채널에 던지면 끝
서버가 받아서 처리하고 채널에 뿌림
구독한 사람들이 알아서 받아가는 것
나도 구독 중이니까 나도 같이 받음

즉, WebSocket은 나한테만 오는 1:1 응답 구조가 없다.
브로드캐스트로 나도 같이 받는 게 응답 역할을 한다.


그래서 type을 두 개 만드는 이유

WebSocket은 1:1 응답이 없기 때문에 요청 타입과 응답 타입을 따로 만든다.

REST API였으면
POST /ready → 200 OK 끝

WebSocket은
내가 READY 보냄 (요청 타입)
→ 서버가 방 전체한테 READY_CHANGED 브로드캐스트 (응답 타입)
→ 나 포함 방에 있는 모든 사람이 받음
→ 모든 사람 화면에 반영

타입이 두 개인 이유가 바로 이것이다.

READY         → 내가 서버한테 보내는 것  "나 준비했어"
READY_CHANGED → 서버가 방 전체에 뿌리는 것  "player1이 준비했어"

REST API는 요청한 사람한테 바로 응답하면 끝이지만,
WebSocket은 여러 명한테 퍼져야 하기 때문에 별도 타입으로 브로드캐스트하는 것이다.


에러가 났을 때

내가 READY 보냄
→ 서버에서 에러 발생
→ /queue/private 로 나한테만 에러 전송

{
  "type": "ERROR",
  "code": "ROOM_NOT_FOUND"
}

→ 이게 REST API의 400, 404 같은 역할

에러가 날 때만 나한테만 오는 유니캐스트로 전달된다.


STOMP와 SockJS

실무에서 WebSocket을 그대로 쓰기보단 위에 STOMP 프로토콜을 얹어서 쓴다.

WebSocket  → 전화기 (연결 수단)
STOMP      → 대화 규칙 ("발신자 밝히고, 용건 먼저 말하기")
SockJS     → WebSocket 연결 안 될 때 자동으로 다른 방법으로 연결해주는 보조 수단

STOMP를 쓰면 채널(목적지) 기반으로 메시지를 주고받을 수 있어서 관리가 편하다.


채널 구조

STOMP에서 채널은 두 종류로 나뉜다.

/topic/...   → 브로드캐스트 (구독한 전체한테)
/queue/...   → 유니캐스트   (특정 1명한테만)

실제 게임 서비스를 예시로 들면

/topic/room/{roomId}       → 방 전체한테 뿌리는 채널
                             준비 상태, 채팅, 입장/퇴장 등

/topic/room/{roomId}/game  → 게임 중 이벤트 채널
                             명령어 낙하, 점수 업데이트 등

/queue/private             → 나한테만 오는 채널
                             강퇴 알림, 에러, 개인 알림 등

브로드캐스트 vs 유니캐스트

// 브로드캐스트 - 채널 구독한 전체한테
messagingTemplate.convertAndSend(
    "/topic/room/123",
    message
)

// 유니캐스트 - 특정 1명한테만
// STOMP가 내부적으로 /user/dobby/queue/private 로 변환
messagingTemplate.convertAndSendToUser(
    "dobby",
    "/queue/private",
    message
)

구독 등록

채널마다 목적이 다르기 때문에 각각 따로 구독 등록해야 한다.
하나만 구독하면 나머지 채널 메시지는 받을 수 없다.

stompClient.connect({}, () => {

    // 방 전체 채널 구독
    stompClient.subscribe("/topic/room/123", (message) => {
        const data = JSON.parse(message.body)
        // 처리
    })

    // 게임 채널 구독
    stompClient.subscribe("/topic/room/123/game", (message) => {
        const data = JSON.parse(message.body)
        // 처리
    })

    // 개인 채널 구독
    stompClient.subscribe("/queue/private", (message) => {
        const data = JSON.parse(message.body)
        // 처리
    })
})

type 필드로 이벤트 구분

REST API는 URL로 어떤 요청인지 구분한다.

GET /rooms       → 방 목록
POST /rooms/join → 방 입장

WebSocket은 하나의 채널로 여러 종류의 메시지가 오기 때문에
type 필드로 어떤 이벤트인지 구분한다.

stompClient.subscribe("/topic/room/123", (message) => {
    const data = JSON.parse(message.body)

    switch (data.type) {
        case "READY_CHANGED":   // 준비 상태 화면 업데이트
        case "CHAT_MESSAGE":    // 채팅창에 메시지 추가
        case "PLAYER_JOINED":   // 플레이어 목록 업데이트
        case "GAME_STARTED":    // 게임 화면으로 전환
        case "ERROR":           // 에러 처리
    }
})

연결 생명주기

WebSocket은 연결을 맺고 끊는 타이밍이 중요하다.

방 입장 확정   → 연결 시작 + 구독 3개 등록
게임 진행 중   → 연결 유지
게임 종료      → 연결 유지 (대기실로 돌아올 수 있으니까)
홈으로 이동    → 연결 해제

비정상 연결 끊김(브라우저 닫기, 네트워크 끊김)은
서버가 heartbeat(ping/pong)으로 감지해서 자동 처리한다.

// 자동 재연결 설정
stompClient.reconnectDelay = 3000  // 끊기면 3초 후 재연결 시도

REST API vs WebSocket 정리

REST APIWebSocket
연결 방식요청마다 새로 연결한 번 연결 후 유지
방향클라이언트 → 서버만양방향
서버가 먼저 전송❌ 불가능✅ 가능
응답 방식요청한 사람한테 1:1 응답채널에 던지면 구독자 전체가 받음
적합한 상황단발성 요청실시간 지속 통신
REST API가 맞는 상황
→ 로그인, 방 목록 조회, 결과 저장
→ 요청 한 번, 응답 한 번으로 끝나는 것들

WebSocket이 맞는 상황
→ 채팅, 게임 상태 동기화, 실시간 알림
→ 서버가 먼저 쏴야 하거나 여러 명한테 동시에 퍼져야 하는 것들

WebSocket 상에서 방이라는 개념은 따로 없다.

WebSocket 입장에서 방은
/topic/room/{roomId}
이 채널 자체가 방이다.
누군가 이 채널을 구독하는 순간 그 채널이 활성화되고, 구독자가 0명이 되면 자연스럽게 비활성화된다.


REST API → Redis에 방 데이터 생성
WebSocket → 채널 구독 시 자동으로 활성화
별도로 "방을 연다"는 개념이 없음
STOMP의 SimpleBroker가 채널을 자동으로 관리한다. 개발자가 채널을 생성하거나 삭제하는 코드를 짤 필요가 없다.


한 줄 요약

REST API는 요청하면 나한테 바로 응답
WebSocket은 채널에 던지면 구독한 사람들이 받아가는 것
구독은 채널 틀어두기, 발행은 댓글 달기
type은 어떤 이벤트인지 구분하는 라벨

profile
느리게 한걸음

0개의 댓글