
백엔드 개발자를 준비하며 생기는 의문들을 정리한 포스트입니다.
우리가 흔히 사용하는 HTTP는 요청이 있어야만 응답이 가능한 단방향 통신인데
실시간으로 연결이 유지되어 데이터를 주고 받을 수 있는 양방향 통신이 필요할 수 있습니다.
그래서 이번 포스트는 채팅, 실시간 알람, 주식 등에 사용할 수 있는 Web Socket에 대해 알아보려 합니다.
WebSocket은 HTTP와는 다르게 클라이언트와 서버가 양방향으로 지속적인 통신을 할 수 있는 프로토콜 입니다.
예를 들어 채팅, 실시간 알림, 게임 등 실시간 데이터 송수신이 필요한 서비스에 주요 사용됩니다.
일반적으로 HTTP 요청/응답과 달리, 한번 연결하면 서버와 클라이언트가 계속 데이터를 주고 받을 수 있습니다.
WebSocket은 한 번 연결만 하면 클라이언트와 서버가 양방향으로 자유롭게 데이터를 주고 받을 수 있습니다.
특히 실시간성이 중요한 서비스에는 꼭 사용되어야 합니다.
이를 통해 실시간 데이터 송수신이 필요한 서비스에 유용하게 사용할 수 있습니다.
HTTP는 요청-응답 이후 연결이 종료되지만
WebSocket은 지속적인 연결을 유지하며 실시간으로 데이터를 PUSH할 수 있습니다.
오버헤드는 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간과 메모리를 뜻합니다.
WebSocket은 처음에만 헨드셰이크를 한 뒤 이후에는 헤더 없이 소켓 메시지를 주고받기 떼문에
네트워크 오버헤드가 낮고 빠릅니다.
폴링 방식보다 적은 리소스로 더 많은 클라이언트를 처리할 수 있습니다.
| 항목 | WebSocket | HTTP | Polling |
|---|---|---|---|
| 통신 방식 | 양방향 (Full Duplex) | 단방향 (Request → Response) | 단방향, 주기적 요청 |
| 연결 유지 | O (지속 연결) | X (요청마다 연결) | X (반복 연결) |
| 실시간성 | 매우 높음 | 낮음 | 중간 (주기에 따라 다름) |
| 서버 → 클라이언트 전송 | O (Push 가능) | X (불가능) | X (클라이언트가 먼저 요청) |
| 오버헤드 | 낮음 (초기만 Handshake) | 높음 (요청마다 헤더 포함) | 높음 (불필요한 요청 반복) |
| 사용 예 | 채팅, 알림, 게임, 실시간 주식 등 | 로그인, 게시글 조회 등 일반 API | 간단한 알림/업데이트 polling이 가능할 때 |
//Spring Boot WebSocket
implementation("org.springframework.boot:spring-boot-starter-websocket")
//entity
@Entity
class Chat(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val roomId: Long? = null,
@Column(nullable = false, unique = true)
val topic: String
)
// dto
data class ChatMessage(
val topic: String,
val time: String,
val message: String
)
//controller
@GetMapping("/")
fun index(): String {
return "index"
}
@PostMapping("/create/topic")
@ResponseBody
fun createChatRoom(@RequestBody topic: String): String {
val savedChat = chatRepository.save(Chat(topic = topic))
return "Chat room created with topic: ${'$'}{savedChat.topic}"
}
// 특정 주제 메시지 전달
@MessageMapping("/chat/message")
fun sendMessage(message: ChatMessage) {
template.convertAndSend("/sub/" + message.topic, message)
}
@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
//클라 서버 세션 연결
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/chat").setAllowedOriginPatterns("*").withSockJS()
}
//pub과 sub의 데이터 통신 정의
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
//클라가 서버로 통신하는 것을 정의
registry.setApplicationDestinationPrefixes("/pub")
//서버가 클라에게 데이터를 전달할 때
registry.enableSimpleBroker("/sub")
}
}
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Simple Chat</title>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.5.1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
#chat-box { border: 1px solid #ccc; height: 300px; overflow-y: scroll; margin-bottom: 10px; padding: 10px; }
</style>
</head>
<body>
<h2>실시간 채팅</h2>
<label>닉네임: <input type="text" id="from" /></label><br/>
<label>채팅방(topic): <input type="text" id="topic" value="chat" /></label><br/><br/>
<div id="chat-box"></div>
<input type="text" id="message" placeholder="메시지를 입력하세요" />
<button onclick="sendMessage()">보내기</button>
<script>
const socket = new SockJS("/chat");
const stompClient = Stomp.over(socket);
let topic = "chat";
stompClient.connect({}, function () {
document.getElementById("topic").addEventListener("change", function () {
topic = this.value;
});
stompClient.subscribe("/sub/chat", function (msg) {
const message = JSON.parse(msg.body);
const messageBox = document.createElement("div");
messageBox.textContent = `${message.from}: ${message.message}`;
document.getElementById("chat-box").appendChild(messageBox);
});
});
function sendMessage() {
const from = document.getElementById("from").value.trim();
const text = document.getElementById("message").value.trim();
const topicValue = document.getElementById("topic").value.trim();
if (!from || !text || !topicValue) {
alert("닉네임, 메시지, 토픽 모두 입력해주세요!");
return;
}
stompClient.send("/pub/chat/message", {}, JSON.stringify({
from: from,
to: "everyone",
topic: topicValue,
message: text
}));
// 내 메시지도 바로 출력
const messageBox = document.createElement("div");
messageBox.textContent = `${from} (나): ${text}`;
document.getElementById("chat-box").appendChild(messageBox);
document.getElementById("message").value = "";
}
</script>
</body>
</html>

| 구성 요소 | 어노테이션 / 클래스 | 사용 목적 | 언제 사용하는가? |
|---|---|---|---|
| WebSocket 설정 | @EnableWebSocketMessageBrokerWebSocketMessageBrokerConfigurer | WebSocket 메시지 브로커 기능 활성화 | WebSocket 기능을 Spring 애플리케이션에 적용할 때 |
| 메시지 송신 경로 | .setApplicationDestinationPrefixes("/app") | 클라이언트가 서버에 메시지를 보낼 경로 지정 | 클라이언트가 메시지를 서버에 전송할 때 |
| 메시지 구독 경로 | .enableSimpleBroker("/topic", "/queue") | 서버가 클라이언트에게 브로드캐스트할 경로 설정 | 서버가 클라이언트에게 실시간 메시지를 보낼 때 |
| 엔드포인트 등록 | registry.addEndpoint("/ws") | WebSocket 연결을 위한 URL 엔드포인트 등록 | 클라이언트가 최초로 WebSocket 연결을 시도할 때 |
| 메시지 처리 | @MessageMapping("/chat") | 클라이언트로부터 받은 메시지를 처리 | 클라이언트가 특정 목적지로 메시지를 전송했을 때 |
| 메시지 브로드캐스트 | @SendTo("/topic/public") | 처리된 메시지를 구독자에게 전송 | 서버가 여러 클라이언트에게 메시지를 전송할 때 |
| 메시지 DTO | data class ChatMessage(...) | 송수신할 메시지 구조 정의 | 메시지를 직렬화/역직렬화해서 주고받을 때 |
실시간으로 통신해야 하는 서비스가 필요할 때는 Spring Web Socket을 이용하여 구현할 수 있습니다.