
기간: 2026.04.20 - 2026.04.24
커리큘럼: 최종 프로젝트 (HR 인사시스템 SaaS 플랫폼) - member-service 실시간 채팅(MemberChat) 도메인 설계 및 구현
이번 주는 HR 플랫폼 안에서 구성원들이 실시간으로 메시지를 주고받는 채팅 기능을 설계하고 구현했다.
1:1·그룹·공지 채팅방과 참여자 상태 관리, 카카오톡 스타일의 읽음 처리, Redis Pub/Sub 기반 실시간 팬아웃, WebSocket 보안(STOMP 인터셉터 체인), S3 파일 첨부 및 AV 스캔 흐름까지 채팅의 전체 라이프사이클을 처음부터 끝까지 직접 만들었다.
설계 내용은 별도 기술 포스팅에서 상세히 다룰 예정이다.
- 조직도 기반으로 채팅방 개설된 예시 이미지
"채팅이 쉬운 기능 아닌가?"라는 생각으로 시작했다가, 연결·저장·전파·읽음·파일·권한이 동시에 맞물려야 한다는 걸 구현하면서 바로 깨달았다. 어느 하나가 빠지면 나머지가 의미를 잃는 시스템이었다.
STOMP 인터셉터 순서를 뒤바꿨을 때 에러 로그 하나 없이 메시지가 조용히 사라지는 버그를 겪었다. 수십 분을 디버깅하다 코드 한 줄의 순서가 원인이었다는 걸 알았을 때, 잘못 설정하면 에러 없이 그냥 안 된다는 WebSocket 특유의 까다로움을 몸으로 배웠다.
Redis 패턴 구독에서 공식 문서에 없는 동작을 마주쳤다. 라이브러리를 믿되 실제 동작은 직접 확인해야 한다는 걸 다시 한 번 새겼다.
HTTP와 WebSocket은 보안 모델이 다르다. HTTP는 요청마다 토큰을 검증하지만, WebSocket은 연결 시 한 번 확정한 신원을 세션 내내 신뢰한다. 이 원칙을 모르면 클라이언트가 payload를 조작해 타인인 척 메시지를 보낼 수 있는 허점이 생긴다.
수평 확장과 실시간 전파는 처음부터 함께 설계해야 한다. 단일 서버에서는 인메모리 브로커만으로 충분하지만, 서버가 여러 대가 되는 순간 각 인스턴스는 자기에게 연결된 사람만 알기 때문에 외부 허브 없이는 팬아웃이 불가능하다.
단순한 제약 하나가 복잡한 문제를 대체할 수 있다. 읽음 처리에서 lastReadMessageId를 단조 증가로만 갱신한다는 조건 하나가, 복잡한 멀티디바이스 동기화 로직을 통째로 대체했다.
보안은 "허용"보다 "차단"을 설계하는 게 더 효과적일 때가 있다. 파일 업로드를 막는 것보다, 스캔이 완료되어 CLEAN 상태가 된 파일에만 다운로드 URL을 발급하는 방식이 실질적으로 악성 파일의 전파를 막는다.
현재 DB 저장과 Redis 발행이 같은 트랜잭션에 묶여 있어, Redis 장애 시 메시지 저장도 함께 롤백된다. Outbox 패턴으로 저장과 발행을 분리하는 것을 다음 개선 과제로 잡았다.
Rate Limiter를 현재의 Redis INCR + EXPIRE 방식에서 Lua 스크립트 기반 토큰버킷으로 전환해, 창 경계의 burst 문제와 원자성 이슈를 함께 해결할 예정이다.
이번 구현에서 배운 "상태와 이력의 저장 계층을 목적에 따라 분리해야 한다"는 원칙을, 앞으로 설계할 모든 도메인에 기본 원칙으로 적용하겠다.
"채팅 시스템을 만들며 깨달은 것 — 메시지를 전송하는 건 쉽지만, 그 메시지가 모든 서버의 모든 사용자에게 빠짐없이 전달되고, 멀티디바이스에서 읽음선이 일관되게 유지되어야 비로소 '쓸 수 있는' 채팅이 된다."