HTTP 프로토콜의 요청-응답 방식은 클라이언트가 서버에 주기적으로 데이터를 요청해야 하는 폴링(Polling) 방식에 의존합니다. 예를 들어 실시간 주식 시세를 확인하려면 브라우저가 1~2초마다 서버에 HTTP 요청을 보내야 하며, 이는 불필요한 네트워크 부하와 지연 시간을 초래합니다.
WebSocket은 이러한 한계를 극복하는 전이중(Full-Duplex) 통신 프로토콜입니다. 한 번의 핸드셰이크로 지속적인 연결을 수립하면 서버와 클라이언트가 자유롭게 메시지를 교환할 수 있습니다.
적용 사례
- 실시간 채팅 서비스
- 금융 데이터 스트리밍
- 협업 편집 도구
- 물류 및 주문 상태 알림
시스템 아키텍처
본 구현에서는 Spring Boot 기반 백엔드와 Vue3 프론트엔드를 연계하여 STOMP 프로토콜 기반 메시징 환경을 구성합니다.
백엔드 구현
의존성 설정
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
메시지 브로커 설정
@Configuration
@EnableWebSocketMessageBroker
public class PushServerConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/push-stream")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/channel");
config.setApplicationDestinationPrefixes("/request");
}
}
알림 발송 서비스
@Service
public class AlertDispatcher {
private final SimpMessagingTemplate template;
public AlertDispatcher(SimpMessagingTemplate template) {
this.template = template;
}
public void broadcastToAll(String payload) {
template.convertAndSend("/channel/alerts", payload);
}
public void sendToSpecificUser(String memberId, String payload) {
template.convertAndSendToUser(memberId, "/channel/private", payload);
}
}
프론트엔드 구현
필수 패키지 설치
npm install @stomp/stompjs sockjs-client
연결 관리 모듈
import { Client } from '@stomp/stompjs';
import SockJS from 'sockjs-client';
const WS_URL = import.meta.env.VITE_PUSH_URL || 'http://localhost:8080/push-stream';
class StreamConnection {
constructor() {
this.stomp = null;
this.reconnectDelay = 3000;
this.subscription = null;
}
establish(channel, onMessage, onDisconnect) {
this.stomp = new Client({
webSocketFactory: () => new SockJS(WS_URL),
reconnectDelay: this.reconnectDelay,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
onConnect: () => {
this.subscription = this.stomp.subscribe(`/channel/${channel}`, (frame) => {
onMessage(JSON.parse(frame.body));
});
},
onDisconnect: () => {
onDisconnect?.();
},
onStompError: (err) => {
console.error('STOMP 오류:', err);
}
});
this.stomp.activate();
}
terminate() {
this.subscription?.unsubscribe();
this.stomp?.deactivate();
}
}
export const streamConn = new StreamConnection();
컴포넌트 적용
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
import { streamConn } from '@/services/streamConnection';
const handleIncoming = (data) => {
ElMessage.info(`알림 수신: ${data.content}`);
};
const syncData = () => {
// 데이터 동기화 로직
};
onMounted(() => {
streamConn.establish('alerts', handleIncoming, syncData);
});
onUnmounted(() => {
streamConn.terminate();
});
</script>
운영 환경 구성
Nginx 프록시 설정
location /push-stream {
proxy_pass http://internal-api:8080/push-stream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400;
}
보안 강화
인증 인터셉터
@Component
public class TokenValidationInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> msg, MessageChannel channel) {
StompHeaderAccessor headers = StompHeaderAccessor.wrap(msg);
if (StompCommand.CONNECT.equals(headers.getCommand())) {
String bearer = headers.getFirstNativeHeader("X-Auth-Token");
if (bearer == null || !JwtParser.validate(bearer)) {
throw new AccessDeniedException("인증 실패");
}
headers.setUser(new AuthenticatedUser(extractUserId(bearer)));
}
return msg;
}
}
설정 등록
@Override
public void configureClientInboundChannel(ChannelRegistration reg) {
reg.interceptors(new TokenValidationInterceptor());
}
클라이언트 인증 헤더
const client = new Client({
webSocketFactory: () => new SockJS(WS_URL),
connectHeaders: { 'X-Auth-Token': retrieveAuthToken() },
// ...
});
핵심 특성
- STOMP 프로토콜 기반 메시지 라우팅
- 자동 재연결 및 하트비트 메커니즘
- 사용자별/전체 브로드캐스트 지원
- JWT 기반 연결 인증
- 리버스 프록시 환경 호환