학습 목표
- WebSocket 통신 방식의 개념과 동작 원리 이해
- SSE의 단방향 한계를 넘어선 양방향 실시간 통신 구현
- 스프링 부트의 WebSocket API를 활용한 기본 WebSocket 서버 구축
- WebSocket 핸드셰이크 과정과 프로토콜 승격 이해
- 순수 WebSocket의 복잡성과 메시지 처리의 어려움 체험
- 실시간 통신의 진화 과정 이해 (폴링 → SSE → WebSocket → STOMP 로드맵의 세 번째 단계)
사전 기반 지식
- 2단계 SSE 시스템 완전 이해 (단방향 푸시, EventSource API)
- HTTP 프로토콜 한계: 요청-응답 기반의 제약사항
- JavaScript WebSocket API: 브라우저의 WebSocket 클라이언트 사용법
- 비동기 통신 개념: 이벤트 기반 메시지 처리 방식
핵심 개념 이해
1. WebSocket이란?
WebSocket은 하나의 TCP 연결 위에서 전이중(Full-Duplex) 통신을 가능하게 하는 프로토콜입니다. 기존의 HTTP와 달리, 연결이 맺어진 후에는 클라이언트와 서버 모두 언제든지 상대방에게 데이터를 전송할 수 있습니다.
전이중(Full-Duplex) 통신은 양방향으로 동시에 데이터를 주고받는 방식을 말합니다. 반이중(Half-Duplex) 통신은 양방향으로 통신이 가능하지만, 동시에 할 수는 없고 한 번에 한쪽 방향으로만 통신하는 방식입니다.
전이중(Full-Duplex)
전이중 통신은 통화하는 전화처럼, 서로 동시에 말하고 들을 수 있는 방식입니다. WebSocket이 바로 이 전이중 통신을 사용해 클라이언트와 서버가 끊임없이 데이터를 주고받을 수 있도록 합니다.
반이중(Half-Duplex)
반이중 통신은 무전기처럼, 한 사람이 말하는 동안 다른 사람은 듣기만 해야 하는 방식입니다. 동시에 말하면 서로의 목소리가 섞여 알아들을 수 없습니다.
2. 통신 방식 진화 과정
폴링 방식 (1단계)
클라이언트 ──[요청]──> 서버
클라이언트 <──[응답]── 서버 (연결 종료)
(2초 대기)
클라이언트 ──[요청]──> 서버 (반복)
문제점: 불필요한 요청, 지연 발생
SSE 방식 (2단계)
클라이언트 ──[연결 요청]──> 서버
클라이언트 <══════════════ 서버 (연결 유지)
클라이언트 <──[서버 푸시]── 서버
장점: 즉시 전송
한계: 단방향 통신 (서버 → 클라이언트만)
WebSocket 방식 (3단계)
클라이언트 ──[핸드셰이크]──> 서버
클라이언트 <══════════════> 서버 (양방향 연결)
클라이언트 ──[메시지]────> 서버
클라이언트 <──[메시지]──── 서버
장점: 양방향 실시간 통신, 낮은 오버헤드
3. WebSocket vs SSE vs 폴링 비교
| 구분 | 폴링 | SSE | WebSocket |
| 통신 방향 | 단방향 (요청/응답) | 단방향 (서버→클라이언트) | 양방향 |
| 연결 방식 | 매번 새 연결 | HTTP 지속 연결 | TCP 지속 연결 |
| 프로토콜 | HTTP | HTTP | WebSocket |
| 오버헤드 | 높음 | 중간 | 낮음 |
| 실시간성 | 낮음 | 높음 | 최고 |
| 구현 복잡도 | 낮음 | 중간 | 높음 |
| 적용 사례 | 간단한 업데이트 | 알림, 피드 | 채팅, 게임, 협업툴 |
4. WebSocket 핸드셰이크 과정
WebSocket 연결은 HTTP 요청으로 시작되어 WebSocket 프로토콜로 업그레이드됩니다.
1단계: 클라이언트 요청
GET /websocket HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
2단계: 서버 응답
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
3단계: WebSocket 연결 완료
- HTTP에서 WebSocket 프로토콜로 전환
- 이후 모든 통신은 WebSocket 프레임으로 진행
5. WebSocket API 핵심 개념
스프링 부트의 WebSocket 지원
- WebSocketSession: 각 클라이언트 연결을 나타내는 객체
- TextWebSocketHandler: 텍스트 메시지 처리를 위한 핸들러
- WebSocketConfigurer: WebSocket 설정을 위한 인터페이스
주요 특징:
- 상태 관리: 각 연결의 상태를 직접 관리해야 함
- 메시지 형식: 자유로운 메시지 형식 (JSON, 텍스트 등)
- 에러 처리: 연결 에러, 메시지 전송 실패 등 직접 처리
- 세션 관리: 연결된 세션들을 수동으로 관리
6. JavaScript WebSocket API
기본 사용법
// WebSocket 연결 생성
const socket = new WebSocket('ws://localhost:8080/websocket');
// 연결 열림
socket.onopen = function(event) {
console.log('WebSocket 연결 성공');
};
// 메시지 수신
socket.onmessage = function(event) {
console.log('받은 메시지:', event.data);
};
// 메시지 전송
socket.send('Hello Server!');
// 연결 닫힘
socket.onclose = function(event) {
console.log('WebSocket 연결 종료');
};
// 에러 발생
socket.onerror = function(error) {
console.log('WebSocket 에러:', error);
};
WebSocket API 특징:
- 즉시 전송: send() 메서드로 즉시 메시지 전송
- 이벤트 기반: onopen, onmessage, onclose, onerror 이벤트
- 바이너리 지원: 텍스트뿐만 아니라 바이너리 데이터도 전송 가능
- 수동 재연결: SSE와 달리 연결 끊김 시 수동으로 재연결 처리
ChatWebSocketHandler의 역할
WebSocket 연결에서 발생하는 모든 이벤트를 처리하는 담당자입니다.
WebSocketHandler 인터페이스란?
WebSocketHandler는 스프링에서 제공하는 인터페이스로, WebSocket 연결의 생명주기 동안 발생하는 모든 이벤트를 처리하기 위한 메서드들을 정의해놓은 계약서(contract)입니다.
ChatWebsocketHandler
package com.example.class_step_websocket.chat;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@RequiredArgsConstructor
@Component
public class ChatWebsocketHandler implements WebSocketHandler {
private final ChatService chatService; //
private final ConcurrentHashMap<String, WebSocketSession> sessionHashMap = new ConcurrentHashMap<>();
// 처음 연결이 열렸을 때 콜백 됨
// WebSocketSession - 개별 클라이언트와 Websocket 연결을 나타내는 객체
// - 각 클라이언트 마다 고유한 세션 ID를 가지고 있다.
// - 메세지 전송 연결 상태 확인, 속성 저장 등의 기능을 제공해 준다.
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 어떤 일을 해야 할까?
String sessionId = session.getId();
sessionHashMap.put(sessionId, session);
log.info("WebSocket 연결 성공 : {}", sessionId);
session.sendMessage(new TextMessage("서버에 연결이 되었습니다"));
}
// 메세지를 받았을 때 호출 됨
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
String payload = message.getPayload().toString();
log.info("메세지 수신 {}", payload);
// 클라이언트가 어떤 방식으로 메세지를 보내는걸까?
// 메세지 형식에 대한 파싱 - 방 입장, 채팅, 귓속말, 시스템 메세지 등
// 메세지 프로토콜 정의
// 클라이언트에서 CHAT: 이라고 던질 예정
// CHAT: 안녕 반가워~
// RoomId : 1
if (payload.startsWith("CHAT:")) {
String chatMessage = payload.substring(5); // CHAT: <-- 제거 됨
chatService.save(chatMessage);
// 브로드 캐스트 처리
broadcastMessage(chatMessage);
} else {
// 알 수 없는 메세지 프로토콜
session.sendMessage(new TextMessage("ERROR: 알 수 없는 메세지 형식입니다"));
}
}
// 전송 에러 발생 시 호출
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.error("WebSocket 전송 에러 : {}, {}", session.getId(), exception);
sessionHashMap.remove(session.getId());
}
// WebSocket 연결이 닫힐 때 호출
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
sessionHashMap.remove(session.getId());
log.info("WebSocket 연결 종료 : {}", session.getId());
}
/**
* 부분 메세지 지원 여부
* 큰 메세지를 조각으로 나누어 전송하는 방식
* - 채팅 메세지는 보통 짧은 텍스트만 처리하기 때문에 false로 반환
*/
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 연결된 모든 클라이언트에게 메세지를 보내주는 기능
*/
private void broadcastMessage(String message) {
sessionHashMap.entrySet().removeIf(entry -> {
try {
// entry.getKey() <-- sessionId
// entry.getValue() <-- WebSocketSession
entry.getValue().sendMessage(new TextMessage("MESSAGE:" + message));
return false; // 전송 성공 시 false 반환 세션 유지
} catch (Exception e) {
log.warn("메세지 전송 실패 : {} (연결 끊김)",entry.getKey());
return true; // 전송 실패 시 true 반환 --> 세션 제거
}
});
}
}
WebSocket 등록(register) 하기
스프링이 "어떤 URL에서 누가 WebSocket을 처리할지" 알아야 하기 때문입니다.
registry.addHandler(chatWebSocketHandler, "/websocket")
// "/websocket" 주소로 WebSocket 연결 요청이 오면
// chatWebSocketHandler가 처리하겠다고 스프링에게 알려주는 것
package com.example.class_step_websocket.config;
import com.example.class_step_websocket.chat.ChatWebsocketHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@RequiredArgsConstructor
@Configuration
@EnableWebSocket // WebSocket 기능 활성화
public class WebSocketConfig implements WebSocketConfigurer {
private final ChatWebsocketHandler chatWebsocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatWebsocketHandler, "/websocket")
.setAllowedOrigins("*"); // 모든 도메인에서 접속 허용
}
}

index.html 수정
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>채팅 (WebSocket)</title>
<style>
body { margin: 20px; font-family: Arial, sans-serif; }
nav ul { list-style: none; padding: 0; display: flex; gap: 10px; }
nav a { padding: 8px 15px; background: #666; color: white; text-decoration: none; }
.chat-area {
border: 1px solid #ccc;
height: 300px;
padding: 10px;
overflow-y: scroll;
background: #f9f9f9;
}
.message {
padding: 5px 10px;
margin: 3px 0;
background: white;
border-radius: 8px;
max-width: 80%;
}
.message:nth-child(odd) {
background: #e1f5fe;
margin-left: 20%;
}
/* 연결 상태 표시용 스타일 */
.status {
padding: 5px 10px;
border-radius: 3px;
font-size: 12px;
margin-bottom: 10px;
}
.connected { background: #d4edda; color: #155724; }
.disconnected { background: #f8d7da; color: #721c24; }
/* 메시지 입력 폼 스타일 */
.message-form {
margin-top: 15px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
background: #f8f9fa;
}
.message-form input {
padding: 8px;
width: 70%;
margin-right: 10px;
border: 1px solid #ccc;
border-radius: 3px;
}
.message-form button {
padding: 8px 15px;
background: #007bff;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.message-form button:disabled {
background: #6c757d;
cursor: not-allowed;
}
</style>
</head>
<body>
<nav>
<ul>
<li><a href="/">목록</a></li>
<li><a href="/save-form">작성</a></li>
</ul>
</nav>
<h3>채팅 (WebSocket 방식)</h3>
<p style="color: #666; font-size: 13px;">실시간 업데이트</p>
<!-- 연결 상태 표시 영역 -->
<div id="status" class="status disconnected">연결 중...</div>
<!-- 채팅 메시지 표시 영역 -->
<div class="chat-area" id="chatArea">
{{#models}}
<div class="message">{{msg}}</div>
{{/models}}
</div>
<!-- 메시지 입력 폼 -->
<div class="message-form">
<form id="chatForm">
<input type="text" id="msgInput" placeholder="메시지를 입력하세요..." required>
<button type="submit" id="sendButton" disabled>연결 중...</button>
</form>
</div>
<script>
<!-- DOM 요소 참조 -->
const chatArea = document.getElementById('chatArea');
const status = document.getElementById('status');
const msgInput = document.getElementById('msgInput');
const chatForm = document.getElementById('chatForm');
const sendButton = document.getElementById('sendButton');
/*
Websocket URL 형식 정의
- ws:// -> webSocket 프로토콜
- wss:// -> https 개념
- 서버 주소 -> 192.168.0.132:8080
- /websocket -> 서버에서 등록해둔 WebSocket 엔드 포인트 경로
*/
const socket = new WebSocket('ws://192.168.0.132:8080/websocket');
socket.onopen = function(event) {
status.textContent = '실시간 연결됨(WebSocket)';
status.className = 'status connected'
// 버튼 활성화
sendButton.disabled = false;
sendButton.textContent = '전송';
msgInput.focus();
console.log('websocket 연결 성공');
}
/* 메세지 전송 폼 처리
기본 폼 submit 이벤트의 동작을 차단하고 WebSocket 으로 전송할 예정
*/
chatForm.addEventListener('submit', function(e) {
e.preventDefault(); // 기본 폼 제출 동작 차단
const message = msgInput.value.trim(); // 입력 값에서 앞뒤 공백 제거
if(message && socket.readyState === WebSocket.OPEN) {
socket.send('CHAT:' + message);
msgInput.value = '';
msgInput.focus();
console.log('메세지 전송 : ' + message);
} else if(socket.readyState !== WebSocket.OPEN) {
alert('WebSocket 연결이 끊어져 있습니다');
}
});
<!-- 서버로 부터 메세지를 받았을 때 처리 -->
socket.onmessage =function(event) {
const message = event.data; // 서버로 부터 받은 메세지 데이터
console.log('메세지 수신', message);
if(message.startsWith('MESSAGE:')) {
const chatMessage = message.substring(8); // MESSAGE: 문자열 제거
// 동적으로 엘리먼트를 생성
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
messageDiv.textContent = chatMessage;
chatArea.appendChild(messageDiv);
chatArea.scrollTop = chatArea.scrollHeight;
}
}
</script>
</body>
</html>
WebSocket 통신 흐름
- 핸드셰이크: HTTP 요청 → WebSocket 프로토콜 업그레이드
- 연결 유지: TCP 연결을 통한 지속적인 양방향 통신
- 메시지 전송: 클라이언트/서버 모두 언제든지 메시지 전송 가능
- 세션 관리: 서버에서 각 WebSocketSession을 수동으로 관리
- 브로드캐스트: 모든 연결된 세션에 메시지 전송
한 번의 TCP 연결로 계속 통신하는 WebSocket은 매번 연결을 맺고 끊는 HTTP 방식보다 오버헤드가 훨씬 작습니다.
오버헤드(Overhead)란?
오버헤드는 어떤 작업을 수행하는 데 필요한 추가적인 부하, 시간, 또는 자원을 의미합니다. 통신 프로토콜에서는 주로 데이터 전송을 위해 본래의 데이터(Payload) 외에 추가로 소모되는 헤더 정보나 연결 관리 비용을 뜻합니다.
Payload의 의미
Payload라는 단어는 IT 분야에서 매우 광범위하게 사용됨. Payload는 운송되는 화물 또는 수송되는 순수한 정보라는 뜻으로 이해할 수 있습니다. 즉, 어떤 통신이나 데이터 전송에서 실제로 전달하고자 하는 핵심 데이터를 의미합니다.
순수 WebSocket의 복잡성 체험
1. 메시지 형식 정의
// 개발자가 직접 정의해야 하는 메시지 프로토콜
"CHAT:안녕하세요" // 채팅 메시지
"ERROR:잘못된 형식" // 에러 메시지
"MESSAGE:새 메시지" // 브로드캐스트 메시지
2. 복잡한 세션 관리
// 세션 추가/제거를 수동으로 처리
sessions.put(sessionId, session);
sessions.remove(sessionId);
// 에러 발생 시 세션 정리
sessions.entrySet().removeIf(entry -> {
// 복잡한 에러 처리 로직
});
3. 수동 에러 처리
// 자동 재연결이 없어 수동 구현 필요
socket.onclose = function(event) {
setTimeout(() => {
location.reload(); // 단순한 해결책
}, 3000);
};
WebSocket의 한계와 STOMP의 필요성
체험한 복잡성들
- 메시지 형식 정의: "CHAT:", "MESSAGE:" 같은 임시방편적 프로토콜
- 세션 관리의 어려움: 수동으로 세션 추가/제거 처리
- 에러 처리의 복잡성: 연결 끊김, 재연결 등 모든 것을 직접 구현
- 사용자별 전송의 복잡함: 특정 사용자에게만 메시지 전송하기 어려움
STOMP가 해결하는 문제들
다음 단계인 STOMP(Simple Text Oriented Messaging Protocol)에서는:
- 표준화된 메시지 형식: JSON 기반 구조화된 메시지
- 자동 세션 관리: 스프링이 세션 관리 자동화
- 메시지 라우팅: 채널 기반 메시지 구독/발행
- 사용자별 전송: 간단한 어노테이션으로 개별 메시지 전송
다음 단계 학습 로드맵
- 폴링 (완료): 주기적 요청/응답
- SSE (완료): 서버 푸시 (단방향)
- WebSocket (현재): 양방향 통신 (복잡함 체험)
- STOMP (다음): 구조화된 메시징 프로토
'Spring boot > Spring boot websocket' 카테고리의 다른 글
| Spring Boot - STOMP(DTO) 변환 (0) | 2025.09.09 |
|---|---|
| Spring Boot - STOMP 프로토콜 채팅 (0) | 2025.09.05 |
| Spring Boot SSE(Server-Sent Events) 채팅 시스템 (0) | 2025.09.04 |
| Spring Boot 폴링(Polling) 채팅 시스템 (0) | 2025.09.02 |