Realtime Chat / Engineering case

상대가 본 메시지는 먼저 저장돼 있어야 한다

accepted와 persisted를 구분하고 DB commit 뒤에만 broadcast해 실시간 화면과 복구 가능한 기록을 같은 순서로 맞췄습니다.

GitHub에서 코드 보기 ↗
User impact

상대 화면에는 도착했지만 새로고침하면 사라지는 메시지는 연결 성공보다 더 나쁜 경험입니다.

01

어떻게 깨지는가

  • persistence와 broadcast consumer가 독립이면 DB 실패보다 broadcast가 먼저 끝날 수 있습니다.
  • 연결이 끊긴 동안의 메시지는 WebSocket만으로 복구할 수 없습니다.
  • 실시간 payload에 DB ID가 없으면 history와 중복 없이 합치기 어렵습니다.

02

지켜야 할 경계

  • 같은 room의 저장 순서를 진단할 수 있어야 합니다.
  • Redis publish 실패는 성공으로 삼키지 않아야 합니다.
  • 비멤버는 CONNECT 이후 SUBSCRIBE에서도 차단돼야 합니다.
commit 뒤에만 상대에게 보이는 메시지DB ID가 live delivery와 reconnect history를 연결합니다.acceptedconsumecommitpersisted payloadlivereconnectSENDclientMessageIdKafkaroom partitionPersisttransactionDB messagepersisted IDBroadcastafter commitClient mergelive + sync dedupe
  1. clientMessageId로 임시 메시지를 추적합니다.
  2. room partition에서 DB에 저장하고 message ID를 만듭니다.
  3. commit된 payload만 broadcast하고 재연결에도 같은 ID를 사용합니다.
DB ID가 live delivery와 reconnect history를 연결합니다.
commit 뒤에만 상대에게 보이는 메시지
commit 뒤에만 상대에게 보이는 메시지DB ID가 live delivery와 reconnect history를 연결합니다.acceptedconsumecommitpersisted payloadlivereconnectSENDclientMessageIdKafkaroom partitionPersisttransactionDB messagepersisted IDBroadcastafter commitClient mergelive + sync dedupe

03

선택한 설계

  • Kafka consumer가 persistence service commit을 마친 뒤 Redis에 publish합니다.
  • clientMessageId는 optimistic row를, DB ID는 persisted message dedupe를 맡습니다.
  • 재연결 시 마지막 DB ID 이후를 sync API로 반복 조회합니다.

04

감수한 trade-off

  • DB commit 뒤 broadcast라 실시간 지연이 조금 늘 수 있지만 phantom delivery를 제거합니다.
  • at-least-once redelivery를 받아들이고 저장·수신 양쪽에서 dedupe합니다.

05

어떻게 확인했는가

  • DB 실패를 주입한 동안 receiver에는 노출되지 않고, 재시도 뒤 DB와 수신이 한 건으로 수렴합니다.
  • Redis publish 실패 뒤 기존 DB ID로 재전달돼 저장과 수신이 모두 한 건으로 유지됩니다.
  • 재연결 sync가 누락 메시지를 보충하고 비멤버 REST·STOMP 접근은 거부됩니다.

Replay the boundary

글 대신 단계별로 따라보기

Flow 재생

Sources

코드와 테스트로 돌아가기

  • 저장 뒤 전달로 재구성한 제품 흐름 merge

    공개 저장소에서 확인
    근거 ↗
  • 비멤버 STOMP 구독 거부 테스트

    공개 저장소에서 확인
    근거 ↗
  • room 단위 Kafka ordering 테스트

    공개 저장소에서 확인
    근거 ↗
  • persisted ID 이후 동기화 API 테스트

    공개 저장소에서 확인
    근거 ↗
  • optimistic row 치환·DB ID 중복 제거 테스트

    공개 저장소에서 확인
    근거 ↗
  • DB identity·중복 부수 효과 경계 테스트

    공개 저장소에서 확인
    근거 ↗
  • Redis publish 실패 뒤 Kafka redelivery 테스트

    공개 저장소에서 확인
    근거 ↗
  • 발신자 PERSISTED 상태 알림 통합 테스트

    공개 저장소에서 확인
    근거 ↗
  • 두 노드 전달·오프라인·실패 복구 브라우저 E2E

    공개 저장소에서 확인
    근거 ↗
한계와 다음 확인
  • 읽음 receipt broadcast는 이번 범위에 포함하지 않습니다.
  • 전역 순서가 아니라 room 단위 순서와 재연결 경계를 다룹니다.