본문 바로가기

학습 기록 (Learning Logs)/Today I Learned

로컬에서 채팅 테스트

그룹채팅에서 카프카로 보낸 메세지를 받을수잇는지 로컬 테스트 하려고해.

 

  • ✅ Kafka로 메시지를 보냄
  • ✅ WebSocketSender 서비스 있음
  • ❌ WebSocket 클라이언트 UI 없음 (Swagger만 있음)

 

1. 로컬 테스트 환경 서버 몇개 띄어야해?

2. WebSocketSender 컨트롤러는 어떻게 만들어?

3. WebSocketSender를 접속해야하는데 프론트 화면이 없어. swagger만 있어 어떻게 채팅처럼하지?

 

✅ 1. 로컬 테스트 환경: 서버 몇 개 필요한가?

최소한 2개 띄우면 구조 테스트가 가능

구성 설명
🔹 1번 서버 채팅 메시지를 Kafka에 발행 (ChatController, KafkaProducer)
🔹 2번 서버 Kafka 메시지를 수신하여 WebSocketSender로 broadcast (KafkaConsumer, WebSocketSender)

➡ 혹은 하나의 서버에서도 역할을 나눠서 테스트할 수 있어요 (싱글 인스턴스, 여러 스레드).

📦 Kafka는 Docker로 띄우면 편합니다.

docker run -d --name kafka \
  -p 9092:9092 -e KAFKA_ADVERTISED_HOST_NAME=localhost \
  -e KAFKA_ZOOKEEPER_CONNECT=localhost:2181 \
  wurstmeister/kafka

 

 

 

 

✅ 전체 흐름 구조

[Kafka Producer] (chat-messages 토픽)
   ↓
[ChatMessageListener]
   ↓
[WebSocketSender] → /topic/chat/{roomId}
   ↓
[구독 중인 클라이언트들]
ChatMessageListener Kafka에서 메시지 수신
WebSocketSender STOMP 브로커로 메시지 전달 (/topic/chat/{roomId})

서버 ---> 고객에게 보낼 때

StompChatController 클라이언트 → 서버 메시지 테스트용
WebSocketConfig STOMP endpoint 및 브로커 설정

 

 

서버가 고객에게 메시지를 보낼 때

// /topic -> broadcast 용 (채팅방 등 다수 사용자 대상)
// /queue -> 1:1 개인 메시지용 (point-to-point)
registry.enableSimpleBroker("/topic", "/queue");

 

그래서 /topic /queue 로 시작하는구나

@Service
@RequiredArgsConstructor
public class WebSocketSender {
    /**
     * Spring WebSocket은 STOMP 프로토콜 위에서 작동하는데
     * SimpMessageSendingOperations 구현체 내부적으로 STOMP를 알아서 처리함
     */
    private final SimpMessagingTemplate messagingTemplate;

    /**
     * Test > StompChatController > @MessageMapping("/chat") 이 붙은 클라이언트들이 자동으로 메시지를 받음
     * 세션 ID나 커넥션 직접 안 잡아도 됨 ->직접 WebSocket 세션 추적, 사용자 매핑, 메시지 전송 로직 필요 없음
     */
    public void sendToChatRoom(String key, Object payload) {
        messagingTemplate.convertAndSend("/topic/chat/" + key, payload);
    }

    /**
     * Spring은 WebSocket 연결 시 user 정보까지 매핑해줄 수 있어서
     * 특정 사용자한테만 보내는 것도 쉽게 가능
     * 1:1 알림, 귓속말
     */
    public void sendToUser(String userId, Object payload) {
        messagingTemplate.convertAndSendToUser(userId, "/queue/messages", payload);
    }
}

 

자바스크립트도 받아야지, 그래서 

localhost:8080/ws-chat/topic/chat/{roomId} 로 구독 중임.

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Test</title>
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.5.1/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
</head>
<body>
<h2>WebSocket Chat Test</h2>
<div>
    <label>roomId: <input id="roomId" value="53d6a720-fc93-4d4f-b83a-27e890262652"/></label>
</div>
<ul id="messages"></ul>

<script>
    // 서버로부터 클라이언트가 받아야해서 /topic
	const socket = new SockJS("http://localhost:8080/ws-chat");
	const stompClient = Stomp.over(socket);
	const roomId = document.getElementById("roomId").value;

	stompClient.connect({}, () => {
		stompClient.subscribe("/topic/chat/" + roomId, (msg) => {
			const li = document.createElement("li");
			li.textContent = "📩 " + msg.body;
			document.getElementById("messages").appendChild(li);
		});
	});
</script>
</body>
</html>

 

 

 

고객이 서버로 메세지를 보낼 때

registry.setApplicationDestinationPrefixes("/app", "/pub");

 

 

 


 

채팅방처럼 접근하고싶어 서버는 하나띄우고 클라이언트 여러명이 붙고싶은데

🖥 서버 (Spring Boot) → KafkaListener → WebSocketSender
           ↳ 클라이언트1 (SUBSCRIBE /topic/chat/ROOM_ID)
           ↳ 클라이언트2 (SUBSCRIBE /topic/chat/ROOM_ID)
           ↳ 클라이언트3 (SUBSCRIBE /topic/chat/ROOM_ID)

 

✅ 클라이언트 여러 명 붙는 방법

🎯 방법 1: HTML 파일 여러 개 열기

  1. 아까 작성한 ws-test.html 파일을 복사해서 여러 탭/창에서 열어요.
  2. 각각 브라우저 탭에서 roomId를 동일하게 설정 (예: test-room)
  3. 서버로부터 Kafka 메시지를 받으면, 모든 탭에서 동시에 수신됨

✅ 브라우저 탭을 여러 개 열면 "클라이언트 1, 2, 3" 역할을 할 수 있음
✅ 각 탭이 SUBSCRIBE /topic/chat/test-room 하면 브로드캐스트 확인 가능

 

 

http://localhost:8080/ws-test.html

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Test</title>
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.5.1/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
</head>
<body>
<h2>WebSocket Chat Test</h2>
<div>
    <label>roomId: <input id="roomId" value="53d6a720-fc93-4d4f-b83a-27e890262652"/></label>
</div>
<ul id="messages"></ul>

<script>
	const socket = new SockJS("http://localhost:8080/ws-chat");
	const stompClient = Stomp.over(socket);
	const roomId = document.getElementById("roomId").value;

	stompClient.connect({}, () => {
		stompClient.subscribe("/topic/chat/" + roomId, (msg) => {
			const li = document.createElement("li");
			li.textContent = "📩 " + msg.body;
			document.getElementById("messages").appendChild(li);
		});
	});
</script>
</body>
</html>

 

 

 

ws://localhost:8080/ws-chat/425/askf4w5s/websocket

ws://localhost:8080/ws-chat/559/sv4qlts0/websocket

ws://localhost:8080/ws-chat/085/upthrsok/websocket

 

 

 

ws://localhost:8080/ws-chat/425/askf4w5s/websocket

ws://localhost:8080/ws-chat/559/sv4qlts0/websocket

ws://localhost:8080/ws-chat/085/upthrsok/websocket

 

이 경로 뒤에 붙는 425/askf4w5s 이런 게 왜 매번 다르게 보이는가?

 

✅ 이유: SockJS의 세션 관리 방식 때문

🔸 SockJS는 WebSocket을 에뮬레이션하는 fallback 지원 라이브러리예요.

브라우저에서 WebSocket을 지원하지 않거나 프록시 환경에서 문제가 생길 때도 정상 작동을 보장하기 위해 다양한 프로토콜을 섞어 사용합니다.

 

  • → 실제로는 다 같은 /ws-chat endpoint에 연결한 거예요.
  • 이 값들은 SockJS가 자동으로 생성한 내부 세션 경로일 뿐입니다.
  • 서버 쪽에서는
    • @MessageMapping("/chat/{roomId}")
    • @SendTo("/topic/chat/{roomId}")
    • 같은 논리 주소만 신경 씁니다.
  • 실제 WebSocket 경로가 달라도 같은 roomId 구독이면, 정상적으로 브로드캐스트됩니다.

 

🔸 내부적으로는 이렇게 구성됨

/ws-chat/{server_id}/{session_id}/websocket

 

  • {server_id}: 서버 인스턴스를 구분하기 위한 임의 숫자 (랜덤)
  • {session_id}: 각 클라이언트의 고유 세션 ID