콘텐츠 보기
로보택시

실시간 알림 소켓 연결 끊김 시 미수신 메시지의 보관 및 재전송 로직

1월 21, 2026 1분 읽기

증상 확인: 소켓이 끊겼을 때 메시지는 어디로 갔나?

클라이언트-서버 간 실시간 통신(WebSocket, Socket.IO 등)에서 가장 치명적인 문제는 연결의 불안정성입니다. 사용자는 채팅 메시지를 보냈거나, 주문 체결 알림을 기다리는데, 갑자기 “연결이 끊겼습니다”라는 팝업만 보게 됩니다. 이때. 전송 중이거나 상대방이 수신하지 못한 메시지들은 공중 분해됩니다. 증상은 명확합니다. “메시지를 보냈는데 상대방이 못 받았다” 또는 “앱을 다시 켜니 그 사이 알림이 날아갔다”는 사용자 리포트가 발생합니다. 이 문제는 단순한 불편함이 아니라, 금융 알림이나 시스템 모니터링 경고에서는 심각한 손실로 이어집니다.

원인 분석: 네트워크는 믿을 수 없는 계층이다

소켓 연결 끊김의 근본 원인은 대부분 네트워크 환경의 변화에 있습니다. 모바일 데이터와 Wi-Fi 간 전환, 불안정한 통신사 네트워크, 서버의 과부하로 인한 타임아웃, 클라이언트 앱이 백그라운드로 전환되며 OS에 의해 연결이 강제 종료되는 경우 등이 있습니다. 기술적으로, TCP 계층에서의 연결 종료(Connection Reset)나 애플리케이션 계층(핑/퐁 메커니즘)에서의 타임아웃이 직접적 트리거가 됩니다. 핵심은 이 끊김 현상을 ‘예외’가 아닌 ‘반드시 발생할 수 있는 정상 케이스’로 시스템을 설계해야 한다는 점입니다.

해결 방법 1: 기본 안전장치 – 클라이언트 측 임시 보관 (Offline Storage)

가장 즉각적이고 구현 난이도가 낮은 방법입니다. 서버에 의존하기 전에, 클라이언트가 보내는 모든 메시지를 로컬에 임시 저장하는 방식입니다. 이는 발송(Send) 실패에 대한 1차 백업 역할을 합니다.

  1. 메시지 발송 로직 수정: 사용자가 메시지를 보낼 때, 곧바로 소켓을 통해 전송하지 마십시오. 먼저 로컬 스토리지(IndexedDB, AsyncStorage, SQLite)에 ‘대기 중(Pending)’ 상태로 저장합니다.
  2. 소켓 이벤트 리스너 추가: 소켓의 on(‘acknowledge’, …) 이벤트를 구독합니다. 서버에서 특정 메시지 ID에 대한 수신 확인(ACK)을 보내면, 로컬에서 해당 메시지의 상태를 ‘전송 완료(Sent)’로 변경하고 삭제합니다.
  3. 재연결 시 동기화: 소켓 연결이 다시建立(establish)되면, 클라이언트는 로컬 스토리지를 스캔하여 ‘대기 중(Pending)’ 상태인 모든 메시지를 서버로 재전송합니다. 이때 메시지 ID(고유 식별자)를 부여해 중복 수신을 방지해야 합니다.

이 방법의 장점은 서버 변경이 거의 필요없고 사용자 경험을 빠르게 개선할 수 있다는 점입니다. 단점은 클라이언트 기기를 손실하면 데이터도 함께 사라지며, 수신 측의 메시지 누락은 해결하지 못합니다.

해결 방법 2: 근본적 해결 – 서버 측 메시지 큐 및 상태 관리

프로덕션 환경에서 필수적으로 구현해야 할 체계입니다. 서버가 모든 메시지의 생명주기를 관리하고, 클라이언트의 온/오프라인 상태를 추적합니다.

먼저, 데이터베이스에 다음과 같은 테이블 구조를 추가하는 것이 일반적입니다.

  • messages 테이블: 메시지 본문, 발신자, 수신자, 생성일시(timestamp).
  • message_deliveries 테이블 (핵심): message_id, user_id(수신자), delivery_status(‘pending’, ‘delivered’, ‘read’), last_attempt_at(마지막 전송 시도 시간).
  • user_connections 테이블: user_id, socket_id, is_online, last_seen_at.

상태 기반 전송 로직 구현

서버 애플리케이션의 메시지 처리 아키텍처는 데이터의 영속성과 전송 보장성을 동시에 확보할 수 있도록 재설계되어야 합니다. 메시지가 수신되면 시스템은 일차적으로 레코드를 저장하고 모든 수신 대상에 대해 대기 상태를 생성하며, 스모크오일솔트 운영 매뉴얼에 기술된 연결 관리 프로토콜에 따라 수신자의 온라인 여부와 소켓 연결의 유효성을 즉각 검증합니다. 활성화된 연결이 확인되면 즉시 푸시를 실행하여 상태를 완료로 갱신하고, 오프라인이거나 전송이 실패한 항목에 대해서는 대기 상태를 유지하여 데이터 유실을 방지합니다. 이후 클라이언트 재접속 시점에 발생하는 핸드셰이크 단계에서 미전송된 데이터를 일괄적으로 동기화함으로써 통신 환경의 불안정성 속에서도 메시지의 전달 완결성을 유지합니다.

해결 방법 3: 고가용성을 위한 메시지 브로커 도입 (RabbitMQ, Redis Streams)

대규모 동시 접속자와 메시지 처리량을 안정적으로 관리해야 할 때 선택합니다. 애플리케이션 서버와 메시지 저장/전달 로직을 분리하는 아키텍처입니다.

  1. 메시지 브로커 설정: RabbitMQ의 Durable Queue나 Redis Streams를 설정합니다. 이 큐는 서버 재시작 후에도 메시지를 유지하도록 구성해야 합니다.
  2. 발행/구독 모델 적용:
    • 메시지 수신 시, 애플리케이션 서버는 메시지를 수신자별로 라우팅 키를 지정해 브로커의 큐에 발행(Publish)합니다. 각 사용자 전용 큐를 생성하는 것이 이상적입니다.
    • 별도의 메시지 배치 작업(Worker Process)이 각 큐를 구독(Subscribe)합니다.
  3. Worker의 스마트한 배달: Worker는 큐에서 메시지를 가져와(Consume), 현재 연결 관리 서비스(예: Redis에 저장된 소켓 ID 맵)를 확인합니다. 사용자가 온라인이면 즉시 전송하고 메시지를 큐에서 제거(Ack). 오프라인이면 메시지를 큐에 그대로 두고, 일정 주기로 재시도합니다.
  4. 장점: 애플리케이션 서버 확장이 용이하고, 메시지 처리 지연이나 서버 장애 시에도 메시지 유실 위험이 현저히 낮아집니다.

주의사항 및 필수 구현 요소

이 로직들을 구현하기 전에 반드시 고려해야 할 보안 및 데이터 무결성 사항입니다. 누락 시 시스템 신뢰도가 크게 떨어집니다.

메시지 중복 방지 (Idempotency)를 위해 모든 메시지에는 고유 ID(UUID 권장)를 부여해야 합니다. 클라이언트나 서버가 동일 ID의 메시지를 다시 받으면 이미 처리된 것으로 간주하고 무시하는 로직이 필수입니다. 전송 타임아웃과 재시도 정책 또한 중요하며, ‘pending’ 메시지에 대한 재전송 시도는 무한정 해서는 안 됩니다. last_attempt_at 타임스탬프와 최대 재시도 횟수(예: 5회)를 기록하고 초과 시 실패 상태로 변경하며 관리자 알림을 트리거해야 합니다. 시스템의 영속성을 보장하기 위한 데이터 관리 방안을 수립하는 과정에서 국가기록원의 전자기록물 장기 보존 포맷 가이드라인을 검토해 보면, 데이터 유실을 방지하면서도 메시지 수명 주기(TTL)를 설정하여 스토리지 효율성을 극대화하는 관리 원칙이 강조되고 있습니다.

만료된 메시지는 보관용 다른 저장소로 이동하거나 삭제되며, 마지막으로 연결 상태 판단의 정확성을 위해 소켓 연결이 끊겼을 때 user_connections의 is_online 플래그를 즉시 false로 업데이트해야 합니다. 소켓의 on(‘disconnect’) 이벤트는 네트워크 단절 시 즉시 발생하지 않을 수 있으므로, 핑/퐁 heartbeat 메커니즘을 구현하여 데드 커넥션을 정기적으로 정리하는 과정이 수반되어야 합니다.

어두운 디지털 공간에서 단절된 소켓을 찾아 헤매는 빛나는 데이터 메시지 패킷의 모습이다.

전문가 팁: 성능과 신뢰성의 균형 잡기

위의 기본 구조를 구현했다면, 다음 단계는 시스템을 더욱 견고하고 효율적으로 만드는 것입니다. 여기에는 추가적인 엔지니어링 노력이 필요하나, 결과는 확실한 가치를 제공합니다.

모니터링 대시보드 구축: 메시지 상태별 카운트, 평균 전송 지연 시간, 재전송 시도 횟수를 실시간으로 모니터링할 수 있는 대시보드를 만드십시오. 이는 시스템의 건강 상태를 가시화하고, 문제가 발생했을 때 즉시 원인을 파악하는 데 결정적인 도구가 됩니다. 단순한 로그 수집을 넘어, 메트릭 기반의 운영이 시스템 신뢰성의 핵심입니다.

점진적 로딩(Progressive Loading) 적용: 사용자가 오프라인 상태가 길어져 미수신 메시지가 수천 개 쌓였을 경우, 재연결 시 한 번에 모두 전송하면 네트워크와 클라이언트 렌더링에 부하를 줍니다. 대신 최근 50개 또는 100개 단위로 나누어 배치 전송하고, 사용자가 스크롤을 올릴 때 이전 메시지를 로드하는 방식을 고려하십시오. 이는 메시지 타임스탬프와 페이지네이션 쿼리로 구현 가능합니다.

전송 보장 레벨 분리: 모든 메시지가 동일한 수준의 전송 보장을 필요로 하지는 않습니다. 일반 채팅은 ‘최소 한 번(at-least-once)’ 전송으로 충분할 수 있지만, 금융 트랜잭션과 직결된 메시지는 ‘정확히 한 번(exactly-once)’ 전송이 필수적입니다. 특히 베팅 취소/적특 처리가 이미 지급된 롤링 커미션에 미치는 롤백 영향도를 분석하고 이를 알림으로 전송하는 경우, 중복 발송이나 누락은 파트너와의 신뢰에 치명적일 수 있습니다. 따라서 메시지 메타데이터에 따라 브로커 설정이나 데이터베이스 커밋 로직을 분기하는 것이 진정한 프로덕션급 솔루션입니다.