You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Client Server
│ │
├──── CONNECT /ws/chat?token=... ──►│ JWT 검증 + 세션에 memberId 저장
│ │
├──── SUBSCRIBE ────────────────────►│
│ /sub/chat/room/{roomId} │
│ │
├──── SEND /pub/chat/send ─────────►│ 메시지 저장 + 브로드캐스트
│ │
│◄─── MESSAGE ───────────────────────┤ 채팅방 참여자 전체에 전달
│ │
발행 (Publish)
Destination
설명
Payload
/pub/chat/send
메시지 전송
chatRoomId, content, type, clientMessageId
/pub/chat/read
읽음 처리
memberId, chatRoomId, messageId
/pub/chat/typing
타이핑 상태
chatRoomId, typingStatus (TYPING/STOP)
구독 (Subscribe)
Destination
설명
/sub/chat/room/{roomId}
새 메시지 + 읽음 알림 수신
/sub/chat/room/{roomId}/typing
타이핑 상태 수신
/sub/chat/room/{roomId}/delete
메시지 삭제 이벤트 수신
/sub/chat/room/{roomId}/restore
메시지 복구 이벤트 수신
/user/queue/errors
STOMP 에러 응답 수신 (본인에게만 전달)
설계 결정
왜 소프트 삭제 + 낙관적 락인가
채팅 메시지는 삭제 후에도 복구할 수 있어야 하므로 물리적 삭제 대신 isDeleted 플래그를 사용합니다.
동시에 같은 메시지를 수정하는 경우 @Version 필드로 충돌을 감지하고 409 Conflict를 반환합니다.
// 낙관적 락 - 동시 수정 시 충돌 감지@VersionprivateLongversion;
// 소프트 삭제 - 복구 가능한 삭제publicvoidsoftDelete() {
this.isDeleted = true;
}
왜 STOMP 프로토콜인가
WebSocket 위에 STOMP 프로토콜을 사용하여 발행/구독 모델을 구현했습니다.
순수 WebSocket 대비 메시지 라우팅, 채널 관리, 프레임 포맷이 표준화되어 클라이언트 구현이 단순해집니다.
Spring의 SimpleMessageBroker를 사용하며, 향후 멀티 인스턴스 확장 시 Redis Pub/Sub으로 전환할 수 있는 구조입니다.
왜 멱등성 키(clientMessageId)인가
네트워크 불안정 시 클라이언트가 같은 메시지를 재전송할 수 있습니다.
clientMessageId(UUID)를 DB에 UNIQUE 제약으로 걸어 중복 저장을 방지합니다.
WebSocket 발신자 위조 방지
핸드셰이크에서 JWT를 검증하고 세션에 memberId를 저장한 뒤,
메시지 전송 시 클라이언트가 보낸 senderId를 무시하고 세션의 값으로 강제 설정합니다.