1. 요구사항
- 채팅방 단위로 메시지를 주고받을 수 있어야 한다.
- 각 채팅방의 최근 메시지를 미리보기 형태로 채팅방 리스트에서 확인할 수 있어야 한다.
- 메시지 저장 시 WebSocket 성능 저하를 방지하기 위해 비동기 저장 구조를 적용해야 한다.
2. API 명세서
📡 WebSocket 메시지 구조 (STOMP)
각 채팅방에 메시지를 보내고 받는 방법은 다음과 같다.
첫번째 `메시지 전송` 경로는 클라이언트에서 메시지를 서버에 Publish 할 때 사용하는 Application Destination 이자, 클라이언트 입장에서는 서버에게 전송하는 용도의 STOMP 엔드포인트이다.
두번째 `메시지 수신` 경로는 클라이언트에서 서버의 메시지를 Subscribe 할 때 사용하는 Subscribe Destination 이자, 메시지 브로커가 관리하는 Broker Destination 이다.
Function | URI | Description |
메시지 전송 | /app/chat.send.{roomId} | 특정 채팅방에 메시지 전송 |
메시지 수신 | /topic/chatroom/{roomId} | 해당 채팅방의 메시지 구독 |
📄 REST API
Method | URI | Description |
GET | /api/chat/rooms | 모든 채팅방 목록 (최근 메시지 포함) |
GET | /api/chat/rooms/{roomId} | 채팅방 상세 조회 (최근 메시지 포함) |
POST | /api/chat/rooms | 채팅방 생성 |
DELETE | /api/chat/roos/{roomId} | 채팅방 삭제 |
3. 문제 해결
① WebSocket 처리 중 DB 저장이 직접 이루어지면 성능 저하 우려
- 문제: chatMessageRepository.save() 호출이 실시간 메시지 송수신 흐름에 개입될 경우, DB I/O로 인해 WebSocket 이 지연될 가능성이 있었다. 그러나 채팅 기록을 저장하지 않을 수도 없는 상황.
- 해결: BlockingQueue + Background Thread 사용
메시지를 수신하면 enqueue() 메서드를 통해 비동기 큐에 저장한다.
ChatMessageQueueProcessor에서 별도의 쓰레드가 큐를 지켜보다가, queue.take()로 메시지를 꺼내고 저장한다.
@PostConstruct
public void startWorker() {
Executors.newSingleThreadExecutor().submit(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
ChatMessage message = queue.take();
chatMessageRepository.save(message);
}
} catch (Exception e) {
log.error("메시지 저장 중 예외 발생", e);
}
});
}
② 최근 메시지 조회
- 문제: 채팅방을 불러올 때, 해당 채팅방에서 가장 최신 내용을 미리보기로 불러와야 했다.
- 해결:
처음 채팅방을 조회하려고 채팅방 객체를 불러올 때는, 딱 불러올 당시에 가장 최근 메시지를 곁들인 ChatRoomPreviewDto 를 호출한다. 이 Dto 에는 채팅방 객체뿐만이 아니라, 해당 채팅방 객체에 딸려있는 가장 최신 메시지도 함께 담겨있다. 즉, 각 채팅방 객체를 불러올 때, findTopByRoomIdOrderBySentAtDesc() 쿼리를 수행하도록 하였다.
- 문제: 그러나, 처음 불러올 때만 최신화되어 있고,이 상태에서는 채팅방 미리보기에 표시된 최근 메시지가 오래된 내용으로 남게 되는 문제가 있었다. 채팅방 목록이 떠 있는 상태에서도 계속해서 신규 메시지가 올라올 가능성이 다분했다.
- 해결:
이 문제를 해결하기 위해, WebSocket을 통한 실시간 푸시 방식을 도입했다. 사용자가 메시지를 보낼 때, 단순히 해당 채팅방 구독자에게만 메시지를 전송하는 것이 아니라, 모든 사용자에게 브로드캐스트하는 /topic/chatroom 채널에도 메시지를 전송하여
각 클라이언트의 채팅방 목록에서도 최신 메시지가 자동으로 반영되도록 구현했다.
messagingTemplate.convertAndSend("/topic/chatroom", saved);
- 채팅방이 새로 생성될 때도 해당 채널을 통해 브로드캐스트됨
- 이후 메시지 전송 시에도 별도로 최신 메시지를 요약해 해당 채널로 함께 전송하는 구조로 확장할 수 있음
4. 결론 및 추후 개발 사항
결론
이번 개발의 핵심은 초기 채팅방 목록이나 특정 채팅방을 불러올 때, 채팅방 객체 정보뿐만이 아니라, 해당 채팅방 객체의 가장 최근 메시지까지 불러오도록 설계하였다.
- 채팅방 최초 진입 시:
REST API를 통해 각 채팅방별 최신 메시지를 포함한 ChatRoomPreviewDto를 제공하여,
사용자에게 빠른 미리보기 제공이 가능하도록 하였다. - 실시간 메시지 수신 시:
메시지 본문은 채팅방별로 분리된 경로(/topic/chatroom/{roomId})를 통해 전송하고,
이 경로를 통해서 채팅방 목록 API 를 이미 가져온 이후라도, 최신 메시지를 갱신할 수 있다.
다만 개선점: 메시지 전송 시 ChatRoomPreviewDto를 별도로 브로드캐스트하는 구조
채팅방의 메시지는 /topic/chatroom/{roomId} 를 통해 전송하더라도,
채팅방 미리보기용 메시지는 별도의 브로드캐스트(ex. /topic/chatroom/preview) 하여 채팅방 목록에서도 실시간 최신 메시지 갱신이 가능하도록 구성할 수 있다.
추후 개발 계획
- JWT 기반 인증 방식 도입하여 무상태 Stateless 구조 실험
- Redis Pub/Sub 기반 큐 구조로 확장 → 서버 다중화(스케일아웃) 대응
- 채팅 메시지에 파일 전송, 이미지, 이모지 기능 추가
- 메시지 국제화 (현재는 메시지 하드코딩)
- 채팅방별 참여자 목록 관리, 인증 기반 메시징 보한 강화(SendToUser)
- WebSocketSessionDesconnectEvent 활용한 입장/퇴장 알림 추가
- 관리자 전용 시스템 메시지 messagingTemplate.convertAndSend()로 구현
'Project > CloudTalk' 카테고리의 다른 글
[CloudTalk v1] REST API 기반 사용자 식별 + STOMP 채팅방 생성 (0) | 2025.04.06 |
---|---|
STOMP, Simple Text Oriented Messaging Protocol (0) | 2025.03.23 |
[프로젝트 소개 및 설계 자료 조사] 클라우드 톡. (0) | 2025.01.22 |