REST API는 클라이언트가 먼저 요청해야만 서버가 응답할 수 있다.
반면 WebSocket은 한 번 연결하면 서버도 클라이언트도 언제든지 먼저 메시지를 보낼 수 있다.
[REST API - 편지]
클라이언트가 편지 보냄 → 서버가 답장
끝. 연결 종료.
다시 필요하면 또 편지 보냄
[WebSocket - 전화]
한 번 연결하면
서버도 먼저 말할 수 있고
클라이언트도 먼저 말할 수 있음
끊기 전까지 계속 대화 가능
속도가 빠르기 때문이 아니다.
진짜 이유는 서버가 클라이언트한테 먼저 데이터를 보낼 수 있기 때문이다.
REST API를 0.1초마다 폴링해도 기술적으로는 실시간처럼 보일 수 있다.
하지만 이 방식은 두 가지 문제가 있다.
문제 1. 서버가 보낼 준비가 됐어도
클라이언트가 요청하기 전까지 전달이 안 됨
문제 2. 4명이 0.1초마다 요청하면
4명 × 10번/초 = 40번/초 불필요한 요청 발생
WebSocket은 서버가 이벤트가 생기는 순간 바로 쏴줄 수 있어서
불필요한 요청 없이 진짜 실시간이 가능하다.
WebSocket의 핵심 개념 3가지를 유튜브에 비유하면 이렇다.
| 개념 | 유튜브 비유 | 설명 |
|---|---|---|
| 연결 | 유튜브 앱 켜기 | 앱을 켜야 구독도 하고 댓글도 달 수 있듯이, 연결해야 구독/발행 가능 |
| 구독 | 채널 구독 | 구독하면 새 영상 알림 오듯이, 채널 구독하면 서버 메시지 자동 수신 |
| 발행 | 댓글 달기 | 내가 채널에 뭔가를 보내는 행위 |
REST API와 WebSocket의 가장 큰 차이는 응답 방식이다.
REST API → 등기우편
내가 보내면 수신 확인 도장 받음
요청하면 반드시 나한테 1:1 응답이 옴
200 OK or 400 Bad Request
WebSocket → 단체 카톡방에 메시지 보내기
내가 채널에 던지면 끝
서버가 받아서 처리하고 채널에 뿌림
구독한 사람들이 알아서 받아가는 것
나도 구독 중이니까 나도 같이 받음
즉, WebSocket은 나한테만 오는 1:1 응답 구조가 없다.
브로드캐스트로 나도 같이 받는 게 응답 역할을 한다.
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 같은 역할
에러가 날 때만 나한테만 오는 유니캐스트로 전달된다.
실무에서 WebSocket을 그대로 쓰기보단 위에 STOMP 프로토콜을 얹어서 쓴다.
WebSocket → 전화기 (연결 수단)
STOMP → 대화 규칙 ("발신자 밝히고, 용건 먼저 말하기")
SockJS → WebSocket 연결 안 될 때 자동으로 다른 방법으로 연결해주는 보조 수단
STOMP를 쓰면 채널(목적지) 기반으로 메시지를 주고받을 수 있어서 관리가 편하다.
STOMP에서 채널은 두 종류로 나뉜다.
/topic/... → 브로드캐스트 (구독한 전체한테)
/queue/... → 유니캐스트 (특정 1명한테만)
실제 게임 서비스를 예시로 들면
/topic/room/{roomId} → 방 전체한테 뿌리는 채널
준비 상태, 채팅, 입장/퇴장 등
/topic/room/{roomId}/game → 게임 중 이벤트 채널
명령어 낙하, 점수 업데이트 등
/queue/private → 나한테만 오는 채널
강퇴 알림, 에러, 개인 알림 등
// 브로드캐스트 - 채널 구독한 전체한테
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)
// 처리
})
})
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 | WebSocket | |
|---|---|---|
| 연결 방식 | 요청마다 새로 연결 | 한 번 연결 후 유지 |
| 방향 | 클라이언트 → 서버만 | 양방향 |
| 서버가 먼저 전송 | ❌ 불가능 | ✅ 가능 |
| 응답 방식 | 요청한 사람한테 1:1 응답 | 채널에 던지면 구독자 전체가 받음 |
| 적합한 상황 | 단발성 요청 | 실시간 지속 통신 |
REST API가 맞는 상황
→ 로그인, 방 목록 조회, 결과 저장
→ 요청 한 번, 응답 한 번으로 끝나는 것들
WebSocket이 맞는 상황
→ 채팅, 게임 상태 동기화, 실시간 알림
→ 서버가 먼저 쏴야 하거나 여러 명한테 동시에 퍼져야 하는 것들
WebSocket 입장에서 방은
/topic/room/{roomId}
이 채널 자체가 방이다.
누군가 이 채널을 구독하는 순간 그 채널이 활성화되고, 구독자가 0명이 되면 자연스럽게 비활성화된다.
즉
REST API → Redis에 방 데이터 생성
WebSocket → 채널 구독 시 자동으로 활성화
별도로 "방을 연다"는 개념이 없음
STOMP의 SimpleBroker가 채널을 자동으로 관리한다. 개발자가 채널을 생성하거나 삭제하는 코드를 짤 필요가 없다.
REST API는 요청하면 나한테 바로 응답
WebSocket은 채널에 던지면 구독한 사람들이 받아가는 것
구독은 채널 틀어두기, 발행은 댓글 달기
type은 어떤 이벤트인지 구분하는 라벨