실시간 통신의 트래픽 폭주를 막고 신뢰성을 보장하는 아키텍처 구상
Socket.io를 도입해 실시간 주문 상태 동기화를 구현했습니다. 화면이 즉각적으로 갱신되는 마법 같은 경험에 처음에는 기뻤지만, 운영 환경을 모니터링하고 트래픽 증가를 시뮬레이션하면서 현재 아키텍처가 가진 '메시지 유실 리스크' 와 '트래픽 관리의 맹점' 을 뼈저리게 인지하게 되었습니다.The Problem: MVP 소켓 아키텍처의 3가지 시한폭탄
현재 시스템은 빠른 기능 구현(MVP)에 초점을 맞추다 보니, 트래픽과 네트워크 불안정을 고려하지 않은 몇 가지 구조적 결함을 안고 있었습니다.
1. 페이지 진입 기반의 단일 Room (트래픽 폭주)
현재는 사용자가 '주문 페이지'에 진입하면 모두 동일한 order_room이라는 하나의 거대한 방에 입장하도록 설계되어 있습니다.
만약 이 방에 1,000명의 도매상과 화원이 접속해 있다면, 단 1건의 주문 상태가 변경되어도 서버는 1,000명 모두에게 브로드캐스팅(Broadcasting)을 해야 합니다. O(N)의 트래픽 낭비가 발생하며, 스케일업을 하더라도 CPU가 브로드캐스팅 연산에 압도당하는 구조적 병목이 존재했습니다.
2. Fire-and-Forget 방식의 메시지 유실 (신뢰성 부재)
현재 서버는 이벤트를 emit으로 쏘고 끝냅니다. 만약 클라이언트가 모바일 환경이라 터널을 지나며 2~3초간 네트워크가 끊겼거나 브라우저 탭이 비활성화되어 소켓이 잠시 끊어졌다면, 그사이에 발생한 상태 변경 이벤트는 영원히 유실됩니다. B2B 거래에서 주문 상태 누락은 곧바로 금전적 CS로 직결됩니다.
3. 서버 재시작 시의 휘발성 (In-Memory 한계) Socket.io의 기본 어댑터(In-memory)를 사용하고 있어, 배포를 위해 PM2 서버를 재시작하거나 인스턴스가 다운되면 기존의 Room 매핑 정보와 미처리 메시지가 전부 허공으로 증발해버립니다.
The Solution: 트래픽 파티셔닝과 메시지 복구 전략
이러한 문제를 근본적으로 해결하고 대규모 트래픽을 견디기 위해, 저는 다음과 같은 '신뢰성 보장 실시간 아키텍처' 로의 리팩토링 로드맵을 수립했습니다.
1. 도메인/리소스 단위의 Room 파티셔닝 (Traffic Management)
거대한 '페이지 단위'의 Room을 폐기하고, '트랜잭션(주문) 단위' 또는 '유저 단위' 로 Room을 잘게 쪼개는 파티셔닝을 적용해야 합니다.
예를 들어 주문이 생성되면 room:order_{order_id} 또는 room:user_{uuid} 형태로 방을 쪼갭니다. 상태가 변경되면 해당 주문과 직접 연관된 도매상과 화원, 단 2명에게만 타겟팅하여 to(room).emit을 쏩니다. 이렇게 하면 동시 접속자가 1만 명이어도 브로드캐스팅 비용은 O(1)에 가깝게 유지됩니다.
2. Socket ACK와 재시도 큐를 통한 수신 보장
일방적인 emit을 버리고, 클라이언트가 메시지를 정상적으로 수신해 화면을 업데이트했는지 확인하는 ACK(Acknowledgement) 콜백 을 도입해야 합니다.
서버는 이벤트를 발송한 후 지정된 시간(예: 3초) 내에 ACK가 오지 않으면, 해당 메시지를 실패로 간주하고 Dead Letter Queue (또는 재시도 큐) 에 넣은 뒤 클라이언트가 다시 안정적인 상태가 되었을 때 재전송하도록 보정 로직을 추가해야 합니다.
3. Cursor 기반의 메시지 복구 (Message Recovery) 시스템 네트워크 단절이나 서버 재시작 시의 유실을 막기 위해, 소켓 이벤트 전용 Inbox 테이블(또는 Redis Stream) 을 구축해야 합니다.
- 서버는 이벤트를
emit하기 전, 부여된 시퀀스 ID(Cursor)와 함께 이벤트 페이로드를 Redis에 임시 저장합니다. - 클라이언트가 네트워크 단절 후 재접속(Reconnect)할 때, 자신이 마지막으로 받은
cursor ID를 서버로 보냅니다. - 서버는 Redis를 확인하여 클라이언트가 놓친 구간의 메시지들을 일괄적으로 다시 내려줍니다(Catch-up). 이를 통해 서버가 재시작되더라도 클라이언트는 절대 상태 변경을 놓치지 않게 됩니다.
4. 다중 서버 확장을 위한 Redis Adapter 도입 향후 트래픽 증가로 인해 Node.js 인스턴스를 여러 대로 스케일 아웃(Scale-out)할 경우, A 서버에 연결된 도매상과 B 서버에 연결된 화원이 같은 Room에 속해 통신할 수 있도록 Socket.io Redis Adapter 를 도입하여 Pub/Sub 기반의 클러스터링 환경을 완성할 것입니다.
The Result & Retrospective
이러한 구조적 한계와 개선 방안을 분석하면서, "실시간(Real-time) 통신의 핵심은 단순히 '빠르게 보내는 것'이 아니라, 어떤 악조건 속에서도 '반드시 도달하게 만드는 신뢰성(Reliability)'에 있다" 는 것을 뼈저리게 배웠습니다.
초기 MVP 모델에서는 구현의 속도가 중요했기에 단일 Room과 In-memory 방식을 택했지만, 시스템이 성장함에 따라 아키텍처도 반드시 그에 맞춰 진화해야 합니다. 네트워크는 언제나 실패할 수 있다는 전제(Fallacies of distributed computing)하에 방어적으로 시스템을 설계하는 아키텍트로 성장하는 귀중한 계기가 되었습니다.