Skip to content

Commit 7a171a4

Browse files
authored
feat : 웹소켓 mq 사용시 scaleout 환경에서 쓸수 있게끔 설정 변경 (#94)
* feat : 웹소켓 mq 사용시 scaleout 환경에서 쓸수 있게끔 설정 변경 * chore : 채팅 세션 정리중 예외 log info 수준으로 낮춤
1 parent 5132f8b commit 7a171a4

File tree

3 files changed

+177
-163
lines changed

3 files changed

+177
-163
lines changed

src/main/java/org/ezcode/codetest/infrastructure/event/config/WebSocketConfig.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ public void configureMessageBroker(MessageBrokerRegistry registry) {
4949
.setClientLogin(mqUsername)
5050
.setClientPasscode(mqPassword)
5151
.setSystemLogin(mqUsername)
52-
.setSystemPasscode(mqPassword);
52+
.setSystemPasscode(mqPassword)
53+
.setUserDestinationBroadcast("/topic/simp-user-registry")
54+
.setUserRegistryBroadcast("/topic/simp-user-registry");
5355

5456
registry.setApplicationDestinationPrefixes("/chat");
5557
registry.setUserDestinationPrefix("/user");

src/main/java/org/ezcode/codetest/infrastructure/event/listener/WebSocketEventListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public void onApplicationEvent(SessionDisconnectEvent event) {
5151
messageService.handleChatRoomEntryExitMessage(ChatMessageTemplate.CHAT_ROOM_LEFT.format(nickName),
5252
chatRoom.getId());
5353
} catch (Exception e) {
54-
log.warn("SessionDisconnectEvent 처리 중 예외 발생, 채팅 관련 웹소켓 세션이 아닙니다.", e);
54+
log.info("SessionDisconnectEvent 처리 중 예외 발생, 채팅 관련 웹소켓 세션이 아닙니다.", e);
5555
}
5656
}
5757
}

src/main/resources/templates/chat-page.html

Lines changed: 173 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -189,185 +189,197 @@ <h2>채팅방 목록</h2>
189189
const socket = new SockJS('/ws?token=' + encodeURIComponent(token));
190190
stompClient = Stomp.over(socket);
191191

192-
stompClient.connect({}, () => {
193-
const messagesEl = document.getElementById('messages');
194-
const roomListEl = document.getElementById('roomList');
195-
const receiptId = 'sub-1';
192+
// ─── Heartbeat 5분(300,000ms) 설정 ───
193+
stompClient.heartbeat.outgoing = 300000; // 클라이언트→서버 heartbeat 전송 간격
194+
stompClient.heartbeat.incoming = 300000; // 서버→클라이언트 heartbeat 수신 허용 최대 간격
195+
const hbHeader = {'heart-beat': '300000,300000'};
196+
// ────────────────────────────────────
196197

197-
stompClient.subscribe('/user/queue/notification', msg => {
198-
console.log(msg);
199-
});
198+
stompClient.connect(
199+
hbHeader,
200+
() => {
201+
const messagesEl = document.getElementById('messages');
202+
const roomListEl = document.getElementById('roomList');
203+
const receiptId = 'sub-1';
200204

201-
stompClient.subscribe('/user/queue/notifications', msg => {
202-
console.log(msg);
203-
});
204-
205-
// 채팅방 목록 초기 구독 및 오름차순 정렬
206-
stompClient.subscribe('/user/queue/chatrooms', msg => {
207-
let rooms;
208-
try { rooms = JSON.parse(msg.body); } catch { return; }
209-
// roomId 기준 오름차순 정렬
210-
rooms.sort((a, b) => Number(a.roomId) - Number(b.roomId));
211-
roomListEl.innerHTML = '';
212-
rooms.forEach(r => {
213-
const li = document.createElement('li');
214-
li.textContent = `${r.title} (${r.roomId}) — ${r.headCount}명`;
215-
li.dataset.roomId = r.roomId;
216-
roomListEl.appendChild(li);
205+
stompClient.subscribe('/user/queue/notification', msg => {
206+
console.log(msg);
217207
});
218-
}, { receipt: receiptId });
219-
220-
// onreceipt 처리: 입장 메시지 전송
221-
stompClient.onreceipt = frame => {
222-
if (frame.headers['receipt-id'] === receiptId) {
223-
stompClient.send('/chat/enter', {}, '경오');
224-
}
225-
};
226208

227-
// 채팅 메시지 구독
228-
stompClient.subscribe('/user/queue/chat', msg => {
229-
messagesEl.innerHTML = '';
230-
let chats;
231-
try { chats = JSON.parse(msg.body); } catch { return; }
232-
chats.forEach(c => {
233-
const time = new Date(c.time)
234-
.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
235-
const li = document.createElement('li');
236-
li.textContent = `[${time}] ${c.name} (${c.tier}): ${c.message}`;
237-
messagesEl.appendChild(li);
209+
stompClient.subscribe('/user/queue/notifications', msg => {
210+
console.log(msg);
238211
});
239-
messagesEl.scrollTop = messagesEl.scrollHeight;
240-
});
241-
242-
// 채팅방 목록 변경 브로드캐스트 구독 (업데이트 시 오름차순 정렬 유지)
243-
stompClient.subscribe('/topic/chatrooms', msg => {
244-
let room;
245-
try { room = JSON.parse(msg.body); } catch { return; }
246212

247-
if (room.eventType === 'CREATE') {
248-
// 신규 방 append (중복 방지)
249-
if (!roomListEl.querySelector(`li[data-room-id="${room.roomId}"]`)) {
250-
const li = document.createElement('li');
251-
li.textContent = `${room.title} (${room.roomId}) — ${room.headCount}명`;
252-
li.dataset.roomId = room.roomId;
253-
roomListEl.appendChild(li);
254-
}
255-
// 오름차순 정렬
256-
const reordered = Array.from(roomListEl.children).sort((a, b) =>
257-
Number(a.dataset.roomId) - Number(b.dataset.roomId)
258-
);
213+
// 채팅방 목록 초기 구독 및 오름차순 정렬
214+
stompClient.subscribe('/user/queue/chatrooms', msg => {
215+
let rooms;
216+
try { rooms = JSON.parse(msg.body); } catch { return; }
217+
// roomId 기준 오름차순 정렬
218+
rooms.sort((a, b) => Number(a.roomId) - Number(b.roomId));
259219
roomListEl.innerHTML = '';
260-
reordered.forEach(li => roomListEl.appendChild(li));
261-
} else if (room.eventType === 'UPDATE') {
262-
// 방 정보 변경, 없으면 추가
263-
let li = roomListEl.querySelector(`li[data-room-id="${room.roomId}"]`);
264-
if (li) {
265-
li.textContent = `${room.title} (${room.roomId}) — ${room.headCount}명`;
266-
} else {
267-
li = document.createElement('li');
268-
li.textContent = `${room.title} (${room.roomId}) — ${room.headCount}명`;
269-
li.dataset.roomId = room.roomId;
220+
rooms.forEach(r => {
221+
const li = document.createElement('li');
222+
li.textContent = `${r.title} (${r.roomId}) — ${r.headCount}명`;
223+
li.dataset.roomId = r.roomId;
270224
roomListEl.appendChild(li);
225+
});
226+
}, { receipt: receiptId });
227+
228+
// onreceipt 처리: 입장 메시지 전송
229+
stompClient.onreceipt = frame => {
230+
if (frame.headers['receipt-id'] === receiptId) {
231+
stompClient.send('/chat/enter', {}, '경오');
271232
}
272-
// 오름차순 정렬
273-
const reordered = Array.from(roomListEl.children).sort((a, b) =>
274-
Number(a.dataset.roomId) - Number(b.dataset.roomId)
275-
);
276-
roomListEl.innerHTML = '';
277-
reordered.forEach(li => roomListEl.appendChild(li));
278-
} else if (room.eventType === 'DELETE') {
279-
// 삭제
280-
const li = roomListEl.querySelector(`li[data-room-id="${room.roomId}"]`);
281-
if (li) li.remove();
282-
} else if (room.eventType === 'GET') {
283-
// 전체 교체 (room.rooms는 방 목록 배열)
284-
if (!Array.isArray(room.rooms)) return;
285-
roomListEl.innerHTML = '';
286-
room.rooms
287-
.sort((a, b) => Number(a.roomId) - Number(b.roomId))
288-
.forEach(r => {
233+
};
234+
235+
// 채팅 메시지 구독
236+
stompClient.subscribe('/user/queue/chat', msg => {
237+
messagesEl.innerHTML = '';
238+
let chats;
239+
try { chats = JSON.parse(msg.body); } catch { return; }
240+
chats.forEach(c => {
241+
const time = new Date(c.time)
242+
.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
243+
const li = document.createElement('li');
244+
li.textContent = `[${time}] ${c.name} (${c.tier}): ${c.message}`;
245+
messagesEl.appendChild(li);
246+
});
247+
messagesEl.scrollTop = messagesEl.scrollHeight;
248+
});
249+
250+
// 채팅방 목록 변경 브로드캐스트 구독 (업데이트 시 오름차순 정렬 유지)
251+
stompClient.subscribe('/topic/chatrooms', msg => {
252+
let room;
253+
try { room = JSON.parse(msg.body); } catch { return; }
254+
255+
if (room.eventType === 'CREATE') {
256+
// 신규 방 append (중복 방지)
257+
if (!roomListEl.querySelector(`li[data-room-id="${room.roomId}"]`)) {
289258
const li = document.createElement('li');
290-
li.textContent = `${r.title} (${r.roomId}) — ${r.headCount}명`;
291-
li.dataset.roomId = r.roomId;
259+
li.textContent = `${room.title} (${room.roomId}) — ${room.headCount}명`;
260+
li.dataset.roomId = room.roomId;
292261
roomListEl.appendChild(li);
293-
});
294-
}
295-
});
262+
}
263+
// 오름차순 정렬
264+
const reordered = Array.from(roomListEl.children).sort((a, b) =>
265+
Number(a.dataset.roomId) - Number(b.dataset.roomId)
266+
);
267+
roomListEl.innerHTML = '';
268+
reordered.forEach(li => roomListEl.appendChild(li));
269+
} else if (room.eventType === 'UPDATE') {
270+
// 방 정보 변경, 없으면 추가
271+
let li = roomListEl.querySelector(`li[data-room-id="${room.roomId}"]`);
272+
if (li) {
273+
li.textContent = `${room.title} (${room.roomId}) — ${room.headCount}명`;
274+
} else {
275+
li = document.createElement('li');
276+
li.textContent = `${room.title} (${room.roomId}) — ${room.headCount}명`;
277+
li.dataset.roomId = room.roomId;
278+
roomListEl.appendChild(li);
279+
}
280+
// 오름차순 정렬
281+
const reordered = Array.from(roomListEl.children).sort((a, b) =>
282+
Number(a.dataset.roomId) - Number(b.dataset.roomId)
283+
);
284+
roomListEl.innerHTML = '';
285+
reordered.forEach(li => roomListEl.appendChild(li));
286+
} else if (room.eventType === 'DELETE') {
287+
// 삭제
288+
const li = roomListEl.querySelector(`li[data-room-id="${room.roomId}"]`);
289+
if (li) li.remove();
290+
} else if (room.eventType === 'GET') {
291+
// 전체 교체 (room.rooms는 방 목록 배열)
292+
if (!Array.isArray(room.rooms)) return;
293+
roomListEl.innerHTML = '';
294+
room.rooms
295+
.sort((a, b) => Number(a.roomId) - Number(b.roomId))
296+
.forEach(r => {
297+
const li = document.createElement('li');
298+
li.textContent = `${r.title} (${r.roomId}) — ${r.headCount}명`;
299+
li.dataset.roomId = r.roomId;
300+
roomListEl.appendChild(li);
301+
});
302+
}
303+
});
296304

297-
// 채팅방 클릭 핸들러
298-
roomListEl.addEventListener('click', e => {
299-
const li = e.target;
300-
const nr = li.dataset.roomId;
301-
if (!nr || currentRoomId === nr) return;
302-
if (chatSubscription && currentRoomId) {
303-
stompClient.send(`/chat/room/${currentRoomId}/left`, {}, currentRoomId);
304-
chatSubscription.unsubscribe();
305-
}
306-
currentRoomId = nr;
307-
roomListEl.querySelectorAll('li').forEach(el => el.classList.remove('active'));
308-
li.classList.add('active');
309-
messagesEl.innerHTML = '';
305+
// 채팅방 클릭 핸들러
306+
roomListEl.addEventListener('click', e => {
307+
const li = e.target;
308+
const nr = li.dataset.roomId;
309+
if (!nr || currentRoomId === nr) return;
310+
if (chatSubscription && currentRoomId) {
311+
stompClient.send(`/chat/room/${currentRoomId}/left`, {}, currentRoomId);
312+
chatSubscription.unsubscribe();
313+
}
314+
currentRoomId = nr;
315+
roomListEl.querySelectorAll('li').forEach(el => el.classList.remove('active'));
316+
li.classList.add('active');
317+
messagesEl.innerHTML = '';
310318

311-
setTimeout(() => {
312-
chatSubscription = stompClient.subscribe(`/topic/chat/${nr}`, m => {
313-
setTimeout(() => {
314-
let p;
315-
try { p = JSON.parse(m.body); } catch {
316-
const lf = document.createElement('li');
317-
lf.textContent = m.body;
318-
messagesEl.appendChild(lf);
319+
setTimeout(() => {
320+
chatSubscription = stompClient.subscribe(`/topic/chat/${nr}`, m => {
321+
setTimeout(() => {
322+
let p;
323+
try { p = JSON.parse(m.body); } catch {
324+
const lf = document.createElement('li');
325+
lf.textContent = m.body;
326+
messagesEl.appendChild(lf);
327+
messagesEl.scrollTop = messagesEl.scrollHeight;
328+
return;
329+
}
330+
const { time, name, tier, message } = p;
331+
const ft = new Date(time)
332+
.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
333+
const liB = document.createElement('li');
334+
liB.textContent = `[${ft}] ${name} (${tier}): ${message}`;
335+
messagesEl.appendChild(liB);
319336
messagesEl.scrollTop = messagesEl.scrollHeight;
320-
return;
321-
}
322-
const { time, name, tier, message } = p;
323-
const ft = new Date(time)
324-
.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
325-
const liB = document.createElement('li');
326-
liB.textContent = `[${ft}] ${name} (${tier}): ${message}`;
327-
messagesEl.appendChild(liB);
328-
messagesEl.scrollTop = messagesEl.scrollHeight;
337+
}, 100);
338+
});
339+
setTimeout(() => {
340+
stompClient.send(`/chat/room/${nr}/enter`, {}, nr);
329341
}, 100);
330-
});
331-
setTimeout(() => {
332-
stompClient.send(`/chat/room/${nr}/enter`, {}, nr);
333342
}, 100);
334-
}, 100);
335-
});
343+
});
336344

337-
// 전송 버튼 클릭 시 fetch 호출
338-
const sendBtn = document.getElementById('sendBtn');
339-
const msgInput = document.getElementById('msg');
340-
msgInput.addEventListener('keydown', e => {
341-
if (e.key === 'Enter') { e.preventDefault(); sendBtn.click(); }
342-
});
343-
sendBtn.addEventListener('click', () => {
344-
if (!currentRoomId) return;
345-
const m = msgInput.value.trim();
346-
if (!m) return;
347-
fetch(`/api/room/${currentRoomId}/chat`, {
348-
method: 'POST',
349-
headers: {
350-
'Content-Type': 'application/json',
351-
'Authorization': 'Bearer ' + token
352-
},
353-
body: JSON.stringify({ message: m })
354-
})
355-
.then(res => {
356-
if (!res.ok) throw new Error('메시지 전송 실패');
357-
return res.json();
358-
})
359-
.then(d => {
360-
const t = new Date(d.time)
361-
.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
362-
const li = document.createElement('li');
363-
li.textContent = `[${t}] ${d.name} (${d.tier}): ${d.message}`;
364-
messagesEl.appendChild(li);
365-
messagesEl.scrollTop = messagesEl.scrollHeight;
345+
// 전송 버튼 클릭 시 fetch 호출
346+
const sendBtn = document.getElementById('sendBtn');
347+
const msgInput = document.getElementById('msg');
348+
msgInput.addEventListener('keydown', e => {
349+
if (e.key === 'Enter') { e.preventDefault(); sendBtn.click(); }
350+
});
351+
sendBtn.addEventListener('click', () => {
352+
if (!currentRoomId) return;
353+
const m = msgInput.value.trim();
354+
if (!m) return;
355+
fetch(`/api/room/${currentRoomId}/chat`, {
356+
method: 'POST',
357+
headers: {
358+
'Content-Type': 'application/json',
359+
'Authorization': 'Bearer ' + token
360+
},
361+
body: JSON.stringify({ message: m })
366362
})
367-
.catch(err => console.error(err));
368-
msgInput.value = '';
369-
});
370-
});
363+
.then(res => {
364+
if (!res.ok) throw new Error('메시지 전송 실패');
365+
return res.json();
366+
})
367+
.then(d => {
368+
const t = new Date(d.time)
369+
.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
370+
const li = document.createElement('li');
371+
li.textContent = `[${t}] ${d.name} (${d.tier}): ${d.message}`;
372+
messagesEl.appendChild(li);
373+
messagesEl.scrollTop = messagesEl.scrollHeight;
374+
})
375+
.catch(err => console.error(err));
376+
msgInput.value = '';
377+
});
378+
},
379+
error => {
380+
console.error('STOMP 연결 에러', error);
381+
}
382+
);
371383
}
372384
</script>
373385
</body>

0 commit comments

Comments
 (0)