Vue3와 Spring Boot 기반 실시간 알림 시스템 구축

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 기반 연결 인증
  • 리버스 프록시 환경 호환

태그: vue3 Spring Boot websocket STOMP SockJS

7월 8일 02:51에 게시됨