Java

[WebSocket] Java로 WebSocket 구현하기(feat. JWT)

연신내고독한늑대 2025. 1. 23. 20:00

이 전 포스팅에서 웹소켓이 무엇이고 웹소켓에 jwt를 추가하는 글을 작성했었는데, 이 번 글에서는 Spring Boot를 사용해 WebSocket 기반의 실시간 통신을 구현하는 과정을 공유합니다. 이 글에서는 WebSocket 설정, 클라이언트와의 메시징 경로, 그리고 JWT 기반의 인증 처리까지 함께 다룰 예정입니다.

 

■ Gradle 설정

Spring Boot에서 WebSocket을 사용하려면 의존성을 추가해야 합니다. Gradle의 build.gradle 파일에 아래 내용을 추가하세요:

implementation 'org.springframework.boot:spring-boot-starter-websocket'

 

■ 메시지 전송 코드

아래는 WebSocket을 통해 메시지를 전송하는 서비스 클래스입니다.
SimpMessagingTemplate을 사용해 특정 사용자 또는 모든 사용자에게 메시지를 전송합니다.

## MessageSender.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

@Service
public class MessageSender {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    // 특정 사용자에게 메시지 전송
    public void sendMessageToUser(String userId, String message) {
        messagingTemplate.convertAndSendToUser(userId, "/queue/match", message);
    }

    // 모든 클라이언트에게 메시지 전송 (브로드캐스트)
    public void sendBroadcastMessage(String topic, String message) {
        messagingTemplate.convertAndSend("/topic/" + topic, message);
    }
}

핵심 내용:

  1. 특정 사용자에게 메시지 전송:
    • convertAndSendToUser 메서드는 /user/{userId}/queue/match 경로로 메시지를 보냅니다.
    • 내부적으로 /user 경로가 기본적으로 붙습니다. 이는 Spring의 SimpMessagingTemplate의 기본 동작입니다.
    • 실제 코드는 다음과 같이 동작(org.springframework:spring-messaging:6.1.10:SimpleMessagingTemplate.java: convertAndSendToUser )
  2. 브로드캐스트 메시지 전송:
    • /topic/{topic} 경로로 메시지를 전송하여, 해당 경로를 구독 중인 모든 사용자에게 메시지를 보냅니다.

 

■ WebSocket 설정

WebSocket 설정은 클라이언트와 서버 간의 경로를 정의하고, 메시징과 인증 처리 방식도 설정합니다. 아래는 WebSocket 설정 코드입니다.

## WebSocketConfig.java

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import taxi.share.back.util.JwtUtil;

@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private final JwtUtil jwtUtil;

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 클라이언트로 메시지를 보낼 브로커 경로 설정
        config.enableSimpleBroker("/user", "/topic"); // 클라이언트가 구독할 경로
        config.setApplicationDestinationPrefixes("/app"); // 클라이언트 → 서버 메시지 경로
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // WebSocket 엔드포인트 설정
        registry.addEndpoint("/ws") // 클라이언트가 WebSocket 연결 요청할 경로
                .addInterceptors(new HttpSessionHandshakeInterceptor(), new CustomHandshakeInterceptor()) // HTTP 헤더 전달
                .setAllowedOriginPatterns("*") // 모든 도메인 허용 (CORS 설정)
                .withSockJS(); // SockJS 지원
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

                // JWT를 쿠키에서 추출
                String jwt = (String) accessor.getSessionAttributes().get("jwt-token");

                // JWT를 검증하고 사용자 정보를 추출
                if (jwt != null) {
                    String userId = jwtUtil.getUserIdByToken(jwt);

                    // 사용자 정보를 Principal로 설정
                    accessor.setUser(new UsernamePasswordAuthenticationToken(userId, null, null));
                    /**
                     * accessor.setUser를 통해 /user/{userId} 경로를 구독할 수 있도록 설정합니다.
                     * 이후 클라이언트가 해당 경로를 구독하면 특정 사용자에게만 메시지를 보낼 수 있습니다.
                     */
                }

                System.out.println("User Info: " + accessor.getUser()); // User가 제대로 설정되었는지 확인
                return message;
            }
        });
    }
}

코드 설명:

  1. 메시지 브로커 설정:
    • 클라이언트가 구독할 경로:
      • /user: 특정 사용자와의 개인 메시징에 사용.
      • /topic: 모든 사용자와의 브로드캐스트 메시징에 사용.
    • 클라이언트 → 서버로 전송되는 경로는 **/app**으로 시작.
  2. STOMP 엔드포인트 등록:
    • /ws: 클라이언트가 WebSocket 연결을 요청하는 엔드포인트.
    • SockJS: WebSocket을 지원하지 않는 환경에서도 사용 가능하도록 백업 옵션 제공.
  3. JWT 인증 처리:
    • WebSocket 연결 시 클라이언트가 전송한 JWT를 쿠키에서 읽어 사용자 ID를 추출.
    • 사용자 ID를 **StompHeaderAccessor**의 Principal로 설정.
    • accessor.setUser를 사용하면 /user/{userId} 경로로 메시지를 구독할 수 있습니다.

 

■ 요약

이 글에서는 WebSocket 설정과 메시지 전송 방식을 설명했습니다. 특히, convertAndSendToUser 메서드를 통해 /user/{userId} 경로로 메시지를 보내는 방법을 다뤘습니다. accessor.setUser 메서드를 사용하여 특정 사용자 경로를 등록하고, 클라이언트가 해당 경로를 구독할 수 있도록 설정했습니다.