웹에서 실시간 이벤트를 처리해야할때 두가지 기술에 대한 이해를 정리했다.
HTML5 표준 기술인 웹 소켓 방식의 기술이 등장하기 전까지는 웹에서 실시간 데이터를 처리해야할 때 마치 실시간인 것처럼 작동하게 하는 방법들이 있었다. 그것이 바로 Polling 방식이다.
HTTP polling과 HTTP long polling은 웹 애플리케이션에서 실시간 데이터 업데이트를 처리하는 두 가지 다른 기술이다.
Pending
)두 방식 모두 실시간 통신을 모방하지만, WebSocket에 비해 효율성이 떨어진다.
HTML5의 출시와 함께 등장한 실시간 양방향 데이터 전송 기술이다.
Opening Handshake
)Closing Handshake
가 진행되며, 클라이언트와 서버 모두 컨트롤 프레임을 전송하여 연결을 종료할 수 있다.WebSocket은 HTTP와 호환되도록 설계되었지만, 두 프로토콜의 아키텍처와 애플리케이션 프로그래밍 모델은 매우 다르다
- HTTP/REST: URL, 메서드, 헤더를 기반으로 요청을 적절한 핸들러로 라우팅
- WebSocket: 초기 연결을 위한 URL이 하나 존재하고, 모든 애플리케이션 내 메시지는 동일한 TCP 연결을 통해 전송
STOMP는 WebSocket의 서브 프로토콜로, 메시지 통신을 위한 형식과 규칙을 정의한다. 기본적으로 Publish-Subscribe 구조를 따르며, frame을 사용해 전송하는 프로토콜이다.
주요 특징:
COMMAND
header1:value1
header2:value2
Body^@
Pub-Sub (Publish-Subscribe) 구조
메시지 통신 패턴으로, 발신자(publisher)가 특정 수신자를 지정하지 않고 메시지를 발행하면,
해당 주제(topic)를 구독한 수신자들(subscribers)에게 자동으로 메시지가 전달되는 방식
구독 예시:
>> SUBSCRIBE id:sub-0 destination:/sub/chat/room/07905aff-a14a-4162-b065-14418519c9d5
메시지 전송 예시:
>> SEND destination:/pub/chat/message content-length:104 {"chatRoomNo":"07905aff-a14a-4162-b065-14418519c9d5","chatMessage":"예시 메세지","chatWriter":"메시지 작성자"}
STOMP를 사용하면 개발자가 메시지 처리 로직을 일일이 구현할 필요 없이, 표준화된 방식으로 실시간 통신을 구현할 수 있다.
STOMP는 Rabbit MQ, Active MQ 등을 사용하여 Pub/Sub 서비스를 이용할 수 있다.
기본적으로 In Memory Broker를 사용할 수 있지만 다음과 같은 단점이 있다:
메시지 브로커의 역할
메시지 브로커는 발신자와 수신자 사이에서 중개자 역할을 함.
메시지의 저장, 전달, 변환 등을 담당
→ 시스템 간의 결합도를 낮추고 확장성을 높일 수 있다.
대규모 실시간 데이터 처리가 필요한 경우, 메시지 브로커로 Apache Kafka를 사용할 수 있다.
(카카오의 대용량 데이터를 처리하는 웹소켓 시스템에 Kafka를 메시지 브로커로 채용한 사례)
Spring에서 WebSocket과 STOMP를 통합하여 사용하는 경우, 다음과 같은 순서로 주요 특징과 구성 요소를 설정해야한다.
먼저 build.gradle 파일에 WebSocket 과 Messaging 관련 의존성을 추가해야한다.
dependencies {
...
//webSocket
implementation 'org.springframework:spring-websocket:5.3.29'
implementation 'org.springframework:spring-messaging:5.3.29'
...
}
WebSocket을 설정하기 위해 WebSocketConfig
클래스를 생성합니다. 이 클래스에서 WebSocket과 STOMP의 주요 설정을 진행합니다:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 메시지 브로커 설정
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 웹소켓 엔드포인트 설정
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
}
configureMessageBroker
: 메시지 브로커를 설정 /topic
과 /queue
는 메시지가 전달될 수 있는 목적지registerStompEndpoints
: 클라이언트가 웹소켓 서버에 연결할 수 있는 엔드포인트를 정의한다. /ws
엔드포인트를 설정하고, SockJS 폴백을 지원하도록 설정했다. setAllowedOrigins("*")
를 통해 모든 도메인에서의 접근을 허용했지만, 실제 운영 환경에서는 보안을 위해 특정 도메인만 허용하는 것이 좋다.이제 WebSocket 메시지를 처리하는 컨트롤러를 만들 차례다.
@MessageMapping
과 @SendTo
를 이용해 STOMP 메시지를 처리한다.
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Controller
public class ChatController {
@MessageMapping("/chat")
@SendTo("/topic/messages")
public OutputMessage send(Message message) {
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
return new OutputMessage(message.getFrom(), message.getText(), time);
}
}
@MessageMapping("/chat")
: 클라이언트에서 /app/chat
으로 전송된 메시지를 이 메서드가 처리한다.@SendTo("/topic/messages")
: 처리된 메시지를 /topic/messages
를 구독하고 있는 모든 클라이언트에게 브로드캐스트한다.전송할 메시지의 구조를 정의하는 클래스 Lombok을 사용하여 보일러 플레이트를 줄였따.
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
private String from;
private String text;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OutputMessage {
private String from;
private String text;
private String time;
}
이제 클라이언트 측에서 WebSocket 통신을 설정한다.
클라이언트는 웹 페이지에서 메시지를 보내고 받을 수 있다.
let stompClient = null;
function connect() {
const socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/messages', function (message) {
showMessage(JSON.parse(message.body));
});
}, function(error) {
console.log('STOMP error ' + error);
});
}
function sendMessage() {
const message = {
from: document.getElementById('from').value,
text: document.getElementById('text').value
};
stompClient.send("/app/chat", {}, JSON.stringify(message));
document.getElementById('text').value = '';
}
function showMessage(message) {
const response = document.getElementById('response');
const p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(`${message.from}: ${message.text} (${message.time})`));
response.appendChild(p);
response.scrollTop = response.scrollHeight; // 새 메시지가 오면 스크롤을 아래로 이동
}
// 페이지 로드 시 WebSocket 연결
window.onload = connect;
connect()
: 서버와의 WebSocket 연결을 설정한다. 연결 실패 시 에러 처리도 추가했다.다음과 같은 HTML 페이지를 작성하여 클라이언트 측에서 메시지를 주고받을 수 있다:
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>실시간 채팅 애플리케이션</title>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs/lib/stomp.min.js"></script>
<script src="/js/app.js"></script>
<style>
#response {
height: 300px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<h1>실시간 채팅</h1>
<div>
<input type="text" id="from" placeholder="이름" required>
<input type="text" id="text" placeholder="메시지" required>
<button onclick="sendMessage();">전송</button>
</div>
<div id="response"></div>
</body>
</html>
모든 설정이 완료되면 스프링 애플리케이션을 실행해 웹소켓 기반 실시간 채팅 앱을 테스트할 수 있다. 여러 브라우저 창에서 같은 URL로 접속해 실시간으로 메시지를 주고받을 수 있다.
STOMP와 Spring WebSocket을 사용해 양방향 실시간 통신을 구현해 보았다.
이 방법으로 클라이언트와 서버 간 실시간 메시지 전송이 가능하며, STOMP로 메시지 브로커와 쉽게 통합할 수 있다. (실제 프로젝트에 적용할 때는 보안, 에러 처리, 스케일링 등을 고려해야 한다.)
최근 공모전에서 쳇봇을 구현하면서 관심있게 찾아본 기술인데 KB_IT's Your LIfe 교육을 듣던 중에 나와서 반가워서 조금 열심히 파봤다.
곧 다가오는 최종 프로젝트에서 이 기술을 적용해 실시간 통신 기능이 있는 서비스를 구현해보고 싶다.
특히 챗봇이나 실시간 알림 시스템 등을 구현하면서, 실제 서비스 환경에서의 문제점들을 발견하고 해결해나가는 경험을 쌓을 수 있으면 좋겠다.
실시간 댓글 개발기(part.1) - DAU 60만 Alex 댓글의 실시간 댓글을 위한 이벤트 기반 아키텍처