포스트

[Project A] WebSocket 기술 기획서

1. 목적

SpringBoot 기반 프로젝트의 실시간 통신 시스템 구현.

순수 WebSocket과 STOMP, Redis Pub/Sub을 활용해 확장 가능하고 안정적인 실시간 통신을 목표로 한다.

예시로 구현한 사용자별 알림 기능뿐만 아니라, LLM + RAG를 통한 사용자 정보 분석 등 엔드 유저의 대기가 발생하는 모든 기능에 대응할 수 있도록 한다.


2. 주요 기능

1) 실시간 WebSocket 통신

  • 순수 WebSocket 기반 연결 (SockJS 미사용)
  • STOMP 프로토콜 지원으로 메시지 라우팅
  • JWT 쿠키 기반 인증 연동

2) 분산 환경 지원

  • Redis Pub/Sub 기반 메시지 브로커
  • 서버 간 메시지 전파 지원
  • 수평 확장 가능한 아키텍처

3) 실시간 알림 시스템

  • 개인별 알림 큐 구독
  • 자동 브라우저 알림 연동
  • 읽음 상태 관리 지원

3. 기술적 선택과 이점

1) WebSocket

장점

  • 네이티브 브라우저 지원으로 높은 성능
  • 프로토콜 오버헤드 최소화
  • 실시간성 극대화
  • 최신 브라우저 환경에서 안정적 동작

고려사항

  • 구형 브라우저 지원 제한
  • 프록시/방화벽 환경에서 일부 제약

2) STOMP 프로토콜 활용

장점

  • 메시지 라우팅 표준화
  • 구독/발행 패턴 지원
  • Spring Framework 네이티브 지원
  • 클라이언트 라이브러리 풍부

주요 기능

  • 목적지 기반 메시지 라우팅
  • 사용자별 개인 큐 지원
  • 메시지 헤더 및 바디 구조화
  • 연결/구독 상태 관리

3) Redis Pub/Sub 분산 처리

아키텍처 이점:

  • 서버 인스턴스 간 메시지 공유
  • 로드밸런서 환경에서 안정적 동작
  • 세션 afinity 불필요
  • 수평적 확장 지원

4. 시스템 아키텍처

1) 전체 구조도

graph TB
    subgraph "Client Layer"
        C1[Client 1<br/>WebSocket]
        C2[Client 2<br/>WebSocket]
        C3[Client N<br/>WebSocket]
    end

    subgraph "Load Balancer"
        LB[Load Balancer<br/>Sticky Session 불필요]
    end

    subgraph "Application Layer"
        A1[API Server 1<br/>WebSocket Handler]
        A2[API Server 2<br/>WebSocket Handler]
        A3[API Server N<br/>WebSocket Handler]
    end

    subgraph "Message Layer"
        R[Redis Pub/Sub<br/>Message Broker]
    end

    subgraph "Database Layer"
        DB[(MySQL<br/>Notification Data)]
    end

    C1 -.-> LB
    C2 -.-> LB
    C3 -.-> LB
    
    LB --> A1
    LB --> A2
    LB --> A3
    
    A1 <--> R
    A2 <--> R
    A3 <--> R
    
    A1 --> DB
    A2 --> DB
    A3 --> DB

2) 계층별 컴포넌트

LayerComponentDescription
PresentationNotificationController알림 전송 REST API
WebSocketWebSocketConfigWebSocket 설정 및 STOMP 구성
InterceptorJwtHandshakeInterceptorWebSocket 핸드셰이크 시 JWT 인증
InterceptorAuthChannelInterceptorSTOMP 채널 인바운드 인증 처리
DomainNotificationPublisherRedis Pub/Sub 메시지 발행
InfrastructureNotificationListenerRedis Pub/Sub 메시지 수신
ApplicationNotificationService알림 비즈니스 로직

5. WebSocket 연결 플로우

1) 초기 연결 과정

sequenceDiagram
    participant Client
    participant LB as Load Balancer
    participant API as API Server
    participant Redis
    participant Auth as JWT Resolver

    Client->>LB: 1. WebSocket 연결 요청<br/>/ws (with cookies)
    LB->>API: 2. 연결 라우팅
    
    API->>Auth: 3. JWT 쿠키 검증
    Auth-->>API: 4. UserPrincipal 반환
    
    API->>API: 5. WebSocket 핸드셰이크 완료
    API->>Client: 6. WebSocket 연결 성공
    
    Client->>API: 7. STOMP CONNECT
    API->>API: 8. 사용자 인증 정보 설정
    API->>Client: 9. STOMP CONNECTED
    
    Client->>API: 10. 개인 큐 구독<br/>SUBSCRIBE /member/queue/notification
    API->>API: 11. 구독 등록 완료

2) 메시지 전송 플로우

sequenceDiagram
    participant Client1
    participant API1 as API Server 1
    participant Redis
    participant API2 as API Server 2
    participant Client2

    Client1->>API1: 1. 알림 전송 요청<br/>POST /notification/send
    API1->>API1: 2. 알림 데이터 DB 저장
    API1->>Redis: 3. Pub/Sub 메시지 발행<br/>member:{memberId}:notification
    
    Redis->>API1: 4. 메시지 브로드캐스트
    Redis->>API2: 5. 메시지 브로드캐스트
    
    API1->>Client1: 6. STOMP 메시지 전송<br/>/member/queue/notification
    API2->>Client2: 7. STOMP 메시지 전송<br/>/member/queue/notification
    
    Client1->>Client1: 8. 브라우저 알림 표시
    Client2->>Client2: 8. 브라우저 알림 표시

6. 상세 구현

1) WebSocket 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 단순 메시지 브로커 활성화
        config.enableSimpleBroker("/topic", "/queue");
        // 애플리케이션 목적지 prefix
        config.setApplicationDestinationPrefixes("/publish");
        // 사용자별 목적지 prefix
        config.setUserDestinationPrefix("/member");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 순수 WebSocket 엔드포인트 (SockJS 미사용)
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns(corsProperties.getAllowedOrigins())
                .addInterceptors(handshakeInterceptor);
    }
}

2) JWT 기반 인증

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class JwtHandshakeInterceptor implements HandshakeInterceptor {
    
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, 
                                   ServerHttpResponse response,
                                   WebSocketHandler wsHandler, 
                                   Map<String, Object> attributes) {
        // JWT 쿠키에서 토큰 추출
        String accessToken = extractTokenFromCookies(request);
        
        // JWT 토큰 검증 및 사용자 정보 추출
        UserPrincipal userPrincipal = jwtResolver.getUserPrincipalFromAccessToken(accessToken);
        
        // WebSocket 세션에 사용자 정보 저장
        attributes.put("USER_PRINCIPAL", userPrincipal);
        return true;
    }
}

3) Redis Pub/Sub 메시지 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class NotificationListener implements MessageListener {
    
    @Override
    public void onMessage(Message message, byte[] pattern) {
        // Redis 메시지를 NotificationMessage로 역직렬화
        NotificationMessage notificationMessage = 
            objectMapper.readValue(new String(message.getBody()), NotificationMessage.class);
        
        // STOMP를 통해 해당 사용자에게 메시지 전송
        simpMessagingTemplate.convertAndSendToUser(
            String.valueOf(notificationMessage.getMemberId()),
            "/queue/notification",
            notificationMessage
        );
    }
}

7. 클라이언트 구현 가이드

1) JavaScript WebSocket 연결

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 순수 WebSocket 연결 (SockJS 미사용)
const websocket = new WebSocket('ws://localhost:8080/ws');

// STOMP over WebSocket
const stompClient = Stomp.over(websocket);

// 연결 설정
stompClient.connect({}, function(frame) {
    console.log('STOMP 연결 성공:', frame);
    
    // 개인 알림 큐 구독
    stompClient.subscribe('/member/queue/notification', function(message) {
        const notification = JSON.parse(message.body);
        handleNotification(notification);
    });
});

2) 알림 처리 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function handleNotification(notification) {
    console.log('새로운 알림:', notification);
    
    // 브라우저 알림 표시
    if ('Notification' in window && Notification.permission === 'granted') {
        new Notification('새로운 알림', {
            body: notification.message,
            icon: '/favicon.ico'
        });
    }
    
    // UI 업데이트
    updateNotificationUI(notification);
}

// 브라우저 알림 권한 요청
function requestNotificationPermission() {
    if ('Notification' in window && Notification.permission !== 'granted') {
        Notification.requestPermission();
    }
}

3) 연결 상태 관리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 연결 상태 모니터링
websocket.onopen = function(event) {
    console.log('WebSocket 연결 성공');
    updateConnectionStatus('connected');
};

websocket.onerror = function(error) {
    console.error('WebSocket 오류:', error);
    updateConnectionStatus('error');
};

websocket.onclose = function(event) {
    console.log('WebSocket 연결 종료:', event.code, event.reason);
    updateConnectionStatus('disconnected');
    
    // 자동 재연결 시도
    setTimeout(reconnectWebSocket, 3000);
};

// 재연결 로직
function reconnectWebSocket() {
    if (websocket.readyState === WebSocket.CLOSED) {
        connectWebSocket();
    }
}

8. 데이터 모델

1) NotificationMessage 구조

1
2
3
4
5
6
{
  "memberId": 1,
  "notificationId": 12345,
  "message": "새로운 알림이 도착했습니다.",
  "redirectPath": "/dashboard"
}

2) Redis Pub/Sub 채널 구조

채널 패턴설명예시
member:{memberId}:notification개인 알림 채널member:1:notification
broadcast:all전체 브로드캐스트broadcast:all

3) WebSocket 세션 속성

1
2
3
4
5
6
Map<String, Object> sessionAttributes = {
    "USER_PRINCIPAL": UserPrincipal,  // 인증된 사용자 정보
    "MEMBER_ID": Long,                // 회원 ID
    "CONNECTED_AT": LocalDateTime,    // 연결 시간
    "LAST_ACTIVITY": LocalDateTime    // 마지막 활동 시간
}

9. 보안 및 인증

1) JWT 쿠키 기반 인증

  • HttpOnly 쿠키로 XSS 공격 방지
  • Secure 플래그로 HTTPS 강제
  • SameSite=None으로 CORS 환경 지원
  • 토큰 만료 시 자동 연결 해제

2) CORS 설정

1
2
3
4
5
6
7
8
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/ws")
            .setAllowedOriginPatterns(
                // Allowed Origins
            )
            .addInterceptors(jwtHandshakeInterceptor);
}

8. 개발 가이드라인

1) 연결 전 확인사항:

  • 브라우저 WebSocket 지원 확인
  • HTTPS 환경에서 WSS 프로토콜 사용
  • 인증 쿠키 설정 확인
  • CORS 정책 준수

2) 구현 시 주의사항:

  • 자동 재연결 로직 구현
  • 연결 상태 UI 표시
  • 메시지 중복 처리 방지
  • 브라우저 알림 권한 요청

16. 참고 자료

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.