Programming/Project

[Project / Collabit] WebSocket + STOMP + Redis로 채팅 구현하기

dev seon 2025. 2. 23. 23:32

 

실시간 채팅 시스템에는 클라이언트와 서버 간의 빠르고 안정적인 메시지 전송이 필요합니다.

이번 프로젝트에서는 WebSocket, Redis, STOMP 기술을 사용해 채팅 기능을 구현하였습니다.

 

서버 측 구현

1. WebSocket 설정

WebSocket은 클라이언트와 서버 간의 양방향 실시간 통신을 제공하는 프로토콜로,

실시간 채팅 기능 구현에서 매우 중요한 역할을 합니다.

Spring에서 WebSocket을 활성화하려면 @EnableWebSocketMessageBroker 어노테이션을 사용하여

STOMP 프로토콜을 통해 메시지를 주고받을 수 있도록 설정합니다.

 

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

    private final StompHandler stompHandler;

//STOMP 엔드포인트 설정
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws/chat")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }

//메시지브로커 설정
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic")
                .setHeartbeatValue(new long[]{10000, 10000})
                .setTaskScheduler(heartBeatScheduler());
        config.setApplicationDestinationPrefixes("/app");
    }

//인터셉터 설정
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(stompHandler);
    }
}


WebSocketConfig에서 WebSocket 연결을 위한 엔드포인트를 /ws/chat으로 설정하였습니다.

이 경로를 통해 클라이언트는 WebSocket에 연결하여 실시간 메시지를 주고받을 수 있습니다.

 

2. Redis 설정 및 사용

Redis는 실시간 데이터 처리를 위한 인메모리 데이터베이스로, 메시지 브로커 역할을 합니다.

Redis의 Pub/Sub 시스템을 사용하여 서버 간 메시지 전달을 효율적으로 처리하고자 했습니다.

 

@RequiredArgsConstructor
@Service
@Slf4j
public class RedisPublisher {

    private final RedisTemplate<String, Object> redisTemplate;

    // Redis 채널에 메시지 발행
    public void publish(String channel, Object message) {
        redisTemplate.convertAndSend(channel, message);
    }
}

 

RedisPublisher에서는 채팅 메시지를 Redis 채널에 발행합니다.

다른 서버나 클라이언트는 이 채널을 구독하고 메시지를 실시간으로 받습니다.

@Slf4j
@RequiredArgsConstructor
@Service
public class RedisSubscriber implements MessageListener {

    private final ObjectMapper objectMapper;
    private final SimpMessagingTemplate messagingTemplate;

    @Override
    public void onMessage(@NonNull Message message, @Nullable byte[] pattern) {
        String body = new String(message.getBody(), StandardCharsets.UTF_8);
        WebSocketMessageDTO webSocketMessage = objectMapper.readValue(body, WebSocketMessageDTO.class);
        String destination = "/topic/chat/" + webSocketMessage.getRoomCode();
        messagingTemplate.convertAndSend(destination, message);
    }
}

 

 

RedisSubscriber는 Redis 채널을 구독하여 메시지를 수신하고, 이를 WebSocket 클라이언트에게 전달합니다.

 

Redis를 사용한 메시지 전달은 WebSocket 서버 간의 메시지 동기화 역할을 하며, 실시간 채팅 환경에서의 성능을 높입니다.

 

3. STOMP 프로토콜을 통한 메시지 전송

STOMP는 WebSocket을 통해 메시지를 주고받기 위한 규약을 제공하며,

메시지의 목적지와 주제를 설정하여 메시지를 효율적으로 관리할 수 있습니다.

 

@MessageMapping 어노테이션을 사용하여 메시지를 처리할 메서드를 설정하고, 클라이언트가 특정 목적지로 메시지를 전송하면 서버는 이를 해당 채팅방에 연결된 클라이언트들에게 전달합니다.

 

@Controller
@RequiredArgsConstructor
@Slf4j
public class WebSocketController {
    // 메시지 전송
    @MessageMapping("/chat.message/{roomCode}")
    @SendTo("/chat/{roomCode}")
    public WebSocketMessageDTO sendMessageToRoom(WebSocketMessageDTO message,
                                                 SimpMessageHeaderAccessor headerAccessor) {
        String userCode = getUserCodeFromHeader(headerAccessor);
        if (message == null || message.getMessage() == null) {
            throw new MessageContentEmptyException();
        }
        webSocketService.handleChatMessage(message, userCode);
        return message;
    }
}

 

클라이언트는 /topic/chat/{roomCode}와 같은 경로를 구독하여 해당 채팅방의 메시지를 실시간으로 수신합니다.

 

 

클라이언트 측 구현

클라이언트는 WebSocket을 통해 실시간으로 서버와 메시지를 주고받으며, API를 통해 필요한 데이터를 로드하고 채팅방의 상태를 업데이트합니다.

1. WebSocket 연결 및 세션 관리

채팅 페이지에 접속하면 WebSocket 연결이 설정됩니다.

이를 통해 클라이언트는 하나의 세션을 유지하며 실시간으로 서버와 통신할 수 있습니다.

세션은 싱글턴 패턴으로 구현하여, 한 번의 연결로 계속 통신할 수 있게 처리했습니다.

2. 채팅방 정보 API와 WebSocket 연결 분리

채팅방 목록, 채팅방 디테일, 메시지 목록 등은 API를 통해 비동기적으로 로드되며,

메시지 송수신은 WebSocket을 통해 처리됩니다.

채팅방 정보를 API로 가져오고, 이후 WebSocket을 통해 해당 채팅방에 메시지를 주고받습니다.

 

3. 채팅방 구독 관리

클라이언트는 채팅 리스트를 불러온 후, 사용자가 속한 모든 채팅방을 구독합니다.

이렇게 하여 해당 채팅방에서 발생하는 메시지를 실시간으로 수신할 수 있습니다.

 

4. 메시지 송신 및 수신

  • 메시지 송신: 클라이언트가 메시지를 입력하면, 해당 메시지를 메시지 목록에 추가하고, 목록을 업데이트합니다.
  • 메시지 수신: 서버에서 메시지를 수신하면, 내가 보낸 메시지가 아닌 경우에만 메시지 목록에 추가하고 이를 업데이트합니다.

5. 읽지 않은 메시지 관리

읽지 않은 메시지는 Redis를 활용해 처리합니다.

클라이언트가 채팅방에 접속하면, 해당 채팅방의 메시지 목록과 읽지 않은 메시지 상태를 API를 통해 업데이트합니다.

 

결론

이전에 WebSocket만을 사용하여 채팅 기능을 구현하였을 때와 달리 많은 고민을 하며 기능을 구현했습니다.

특히 안정적인 실시간 채팅을 위해 단일 세션을 유지할 수 있도록 서버와 클라이언트단에서 신경을 썼습니다.

글에는 담지 않았지만 Redis를 활용한 읽지 않은 메시지 처리 및 SSE를 활용한 알림 기능까지 구현하여

채팅에서 해볼 수 있는 많은 기능을 이번 프로젝트에서 구현해보았습니다.

특히 클라이언트와 서버를 둘 다 맡아 연결 과정에서 필요한 리팩토링을 혼자 진행하며 이해도가 높아졌다고 생각합니다.

이후에 실시간 통신을 구현한다면 단순히 텍스트를 전달하는 것을 넘어 멀티미디어를 공유하는 방법을 알아보고 싶습니다.