본문 바로가기

프로젝트/미니 프로젝트

spring boot + apache kafka 를 활용한 미니 채팅앱 만들어보기(3) - socket.io를 활용한 채팅 구현하기

https://loy124.tistory.com/414

 

spring boot + apache kafka 를 활용한 미니 채팅앱 만들어보기(2) - spring boot 에서 producer, consumer 구현하

https://loy124.tistory.com/413 spring boot + apache kafka 를 활용한 미니 채팅앱 만들어보기(1) - kafka 개요 및 테스트Apache Kafka kafka는 분산 스트리밍 플랫폼으로서 메시지큐 역할을 할 수 있는 시스템이다.대

loy124.tistory.com

 

 

https://github.com/loy124/kafka-chat-server/tree/v1.0.0

 

GitHub - loy124/kafka-chat-server

Contribute to loy124/kafka-chat-server development by creating an account on GitHub.

github.com

 

앞으로 내용은 위 코드를 기반으로 진행할 예정이다. (이전 글에 작성한 내용들) 

 

현재 구현된 내용 

카프카에 데이터 전송하기 클라이언트 → [ChatController] → [ChatService] → [ChatKafkaProducer] → Kafka 토픽(chat-room) 
카프카로부터 데이터 조회하기 kafka 토픽(chat-room) -> [ChatKafkaConsumer] -> [ChatService]

 

현재 상황은 이렇게 /api/chat/send에 데이터를 POST 요청하면

 

 

Consumer에서 해당 코드를 받아 service를 호출성공및 log가 잘 나오는 상태이다.

    @KafkaListener(topics = "chat-room", groupId = "chat-group")
    public void userMessageListener(ConsumerRecord<String, String> record) {
        try {
            ChatMessage message = objectMapper.readValue(record.value(), ChatMessage.class);
            System.out.println("유저용 Kafka 수신 메시지: " + message);
            chatService.handleReceivedMessage(message);  // WebSocket 전송 등
        } catch (Exception e) {
            System.err.println("[chat-group] 처리 실패: " + e.getMessage());
        }
    }

 

 

 

 

 

이제 websocket을 연동해서 클라이언트에 전송할 차례이다. 

 

프로세스 

데이터 조회하기 kafka 토픽(chat-room) -> [ChatKafkaConsumer] -> [ChatService] -> websocket을 통해 클라이언트에 전송

 

폴더구조

 

websocket 연결하기 

 

 

core/config/WebSocketConfig.java

package com.example.chat.chat_server.core.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws-chat")   // 클라이언트 연결 주소
                .setAllowedOriginPatterns("*")
                .withSockJS(); // SockJS fallback
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");   // 메시지 구독 경로
        registry.setApplicationDestinationPrefixes("/app"); // 클라이언트 전송 prefix
    }
}

 

이제 클라이언트에 접근할때 /ws-chat으로 접근해서 소켓을 연결하게 된다.

 

 

 

core/websocket/WebSocketSender.java

package com.example.chat.chat_server.core.websocket;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@RequiredArgsConstructor
public class WebSocketSender {

    private final SimpMessagingTemplate messagingTemplate;

    public void send(String destination, Object payload) {
        try {
            messagingTemplate.convertAndSend(destination, payload);
            log.info("WebSocket 전송 - dest: {}, payload: {}", destination, payload);
        } catch (Exception e) {
            log.error("WebSocket 전송 실패 - dest: {}, error: {}", destination, e.getMessage());
        }
    }


}

 

 

 

클라이언트 만들기 

 

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

    
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>

    <script>
        const socket = new SockJS('http://localhost:8080/ws-chat');
        const stompClient = Stomp.over(socket);

        stompClient.connect({}, () => {
            console.log('WebSocket 연결 완료');

            // 특정 채팅방 구독 (예: room-1001)
            stompClient.subscribe('/topic/chat/room-1', (msg) => {
                const chat = JSON.parse(msg.body);
                console.log("받은 메시지:", chat);
            });
        });
    </script>
</body>
</html>

여기에서 사용되고 있는 stomp는 Spring WebSocket에서 메시지 목적지를 정하고 구독/전송을 구조화할 수 있게 해주는 경량 프로토콜이다. 기본적으로 Spring WebSocket은 stomp 기반으로 구성되어 있어 클라이언트 단에서 

websocket + stomp를 사용하는 구조로 바꿔줘야 한다. 

 

 

 

위 서버를 vscode live-server (vscode extensions) 로 기동해준다. 

 

 

 

 

PostMan에서  /topic/chat-room-1로 데이터를 보내게되면 

 

데이터 전송-> kafka 토픽(chat-room) -> [ChatKafkaConsumer] -> [ChatService] -> websocket을 통해 클라이언트에 전송
(/topic/chat/room-1) -> 클라이언트에서 /topic/chat/room-1 로 구독하여 메시지를 전달 받음

위와같은 프로세스로 동작하게 된다. 

 

받은메시지로 잘 넘어온것을 확인할 수 있다.

 

 

 

클라이언트완성하기 

 

이제 Postman으로 보내던 요청을 client에서도 요청을 보낼수 있게 변경한다.

 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>STOMP 채팅 - 다중 유저</title>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
  <style>
    body { font-family: Arial, sans-serif; padding: 1rem; }
    #chat-box { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 10px; margin-bottom: 10px; }
    .me { text-align: right; color: blue; }
    .other { text-align: left; color: green; }
  </style>
</head>
<body>

<h2>채팅방: room-1</h2>

<label>사용자 이름 (user ID):</label>
<input type="text" id="user-id" value="userA" placeholder="userA 또는 userB" />
<button onclick="connect()">접속</button>

<div id="chat-ui" style="display: none;">
  <div id="chat-box"></div>

  <input type="text" id="message-input" placeholder="메시지를 입력하세요" style="width: 70%">
  <button onclick="sendMessage()">전송</button>
</div>

<script>
  let stompClient = null;
  let userId = '';
  const roomId = 'room-1';

  function connect() {
    userId = document.getElementById('user-id').value.trim();
    if (!userId) {
      alert('사용자 이름을 입력해주세요');
      return;
    }

    const socket = new SockJS('http://localhost:8080/ws-chat');
    stompClient = Stomp.over(socket);
    

    stompClient.connect({}, () => {
      console.log('WebSocket 연결 완료 (유저:', userId, ')');
      document.querySelector('#chat-ui').style.display = 'block';

      stompClient.subscribe(`/topic/chat/${roomId}`, (msg) => {
        console.log(msg)
        const chat = JSON.parse(msg.body);
        displayMessage(chat);
      });
    });
  }

  function sendMessage() {
    const messageInput = document.getElementById('message-input');
    const message = messageInput.value.trim();
    if (!message) return;

    axios.post('http://localhost:8080/api/chat/send', {
      roomId: 1,
      sender: userId,
      message: message
    }).then(() => {
      messageInput.value = '';
    }).catch(err => {
      console.error('전송 실패:', err);
    });
  }

  function displayMessage(chat) {
    const box = document.querySelector('#chat-box');
    const div = document.createElement('div');
    div.className = (chat.sender === userId) ? 'me' : 'other';
    div.innerText = `${chat.sender}: ${chat.message}`;
    box.appendChild(div);
    box.scrollTop = box.scrollHeight;
  }
</script>

</body>
</html>

 

위와 같이 요청하게 되면 

 

cors 이슈가 발생한다.

 

 

CORS

- 기본적으로 브라우저는 다른 출처(IP가 같더라도 포트가 다른경우도 포함) 보안상 차단한다.

현재 클라이언트(vscode-live-server : 5500 port)

 

따라서 백엔드 단에서  해당 CORS에 대한 에러를 해결해주기 위해 추가적으로 설정을 해줘야한다. 

 

 

core/config/CorsConfig.java

package com.example.chat.chat_server.core.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true); // 내 서버가 응답을 할때 json을 자바스크립트에서 처리할수 있게 할지 설정
//        config.addAllowedOrigin("*"); // 모든 ip 응답 허용
        config.addAllowedOriginPattern("*");
        config.addAllowedHeader("*"); // 모든 hedaer 응답 허용
        config.addAllowedMethod("*"); // 모든 post,get,pu,delete,path 요청등 허용
        config.addExposedHeader("Authorization");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }


}

 

 

위와 같이 cors에  대한 설정 완료하고 서버를 다시 키고 테스트를 진행하면 된다. 

 

 

위와 같이 간단한 채팅이 되는것을 확인할 수 있다. 

 

 

해당 부분까지 완성 코드는 다음과 같다.

 

 

https://github.com/loy124/kafka-chat-server/tree/v2.0.0

 

GitHub - loy124/kafka-chat-server

Contribute to loy124/kafka-chat-server development by creating an account on GitHub.

github.com

 

 

kafka-setting -> docker compose kafka

client -> web client html

chat-server-backend -> spring boot kafka + websocket backend

 

반응형