핫챗! 대규모 트래픽 오픈채팅방 대응 전략

릴리김·2026년 3월 5일

study

목록 보기
3/7
post-thumbnail

대규모 단체 오픈채팅 시스템 설계

DAU 5천만, 일일 메시지 30억 건을 처리하는 채팅 시스템 아키텍처

목차

  1. 문제 정의
  2. 기능 요구사항
  3. 비기능 요구사항
  4. 핵심 도전과제
  5. 아키텍처 설계
  6. 핫챗 대응 전략
  7. 메시지 전달 메커니즘
  8. 데이터베이스 전략
  9. 트레이드오프와 결정사항
  10. 미해결 과제

문제 정의

LINE과 KakaoTalk 사례를 참고하여 고트래픽 상황에서도 안정적으로 동작하는 오픈 채팅방 시스템을 설계합니다.

핵심 시나리오

두 가지 극단적 상황 중 메시지 동시 전송 시나리오를 중점으로 설계했습니다.

  1. 핫챗 시나리오 (중점)

    • 방 하나에 5,000명 동시 접속
    • 초당 5,000개 메시지 발생
    • 필요한 Push: 5,000 × 5,000 = 2,500만 건/초
  2. 동시 입장 시나리오 (추후 통합 예정)

    • QR 코드로 2,000명이 동시에 입장
    • MySQL Auto-increment lock 경합 이슈

기능 요구사항

채팅방 관리

  • 채팅방 생성, 삭제
  • 참여 제어 (링크, QR 코드)
  • 입장/퇴장 기능
  • 방장/부방장 권한 관리

커뮤니케이션

  • 텍스트, 이미지, 영상, 파일 전송
  • 멘션 기능
  • 리액션 및 댓글
  • 공지사항

알림

  • 메시지 알림 발송
  • 멘션 알림
  • 공지 알림

데이터 관리

  • 이전 채팅 조회 (시퀀스 기반)
  • 데이터 동기화

비기능 요구사항

트래픽 산정

DAU: 50,000,000명
1인당 일일 메시지: 60-100건
일일 총 메시지: 30억 건 (보수적 추정)
평균 TPS: 57,000 msg/sec
피크 TPS: 5,700,000 msg/sec (평균의 100배)

성능 목표

  • 메시지 전달 지연: < 1초 (P99)
  • API 응답시간: < 300ms (P95)
  • 가용성: 99.9%

WebSocket 서버 규모 산정

동시 접속자 계산

DAU: 5,000만 명
동시 접속률: 40% 가정
실제 동시 접속: 2,000만 명

서버당 수용 가능 연결: 50,000개 (업계 표준: 3-5만)
필요 서버: 2,000만 / 5만 = 400대

최악 시나리오 (100% 동시 접속): 1,000대

핫챗 부하 계산

방 1개 (5,000명, 초당 5,000 메시지):
  메시지 발생: 5,000 msg/sec
  필요한 Push: 5,000 × 5,000 = 25,000,000 Push/sec
  서버당 부하 (400대): 62,500 Push/sec/서버

→ 단순 계산으로는 WebSocket 서버가 감당 불가능!


핵심 도전과제

1. Subscribe 병목

[문제]
메시지 1개 발생
  ↓
400개 WS 서버가 모두 수신 (Redis Pub/Sub)
  ↓
각 서버가 자기 클라이언트에게 Push
  ↓
서버당 62,500 Push/sec 필요
  ↓
CPU/네트워크 과부하!

2. 샤딩 한계

채팅방 ID로 샤딩하면 핫챗의 모든 부하가 한 샤드에 집중

3. 메시지 유실 문제

Redis Pub/Sub은 휘발성 → 서버 다운 시 메시지 유실


아키텍처 설계

전체 구조

[Client 5,000명]
    ↓ WebSocket (읽기 + 쓰기)
[Load Balancer (NLB)]
    ↓
[WebSocket 서버 400대]
    ├─→ Redis Pub/Sub (실시간 알림)
    └─→ Kafka (영속성 보장)
         ↓
    [Worker Server]
         ↓
    [Aurora Master]
         ↓ 복제
    [Aurora Replica 10-15대]

컴포넌트 역할

WebSocket 서버 (400대)

  • 사용자 연결 관리
  • 메시지 수신 및 검증
  • Kafka 발행
  • Redis Pub/Sub 구독
  • 실시간 Push 전달

Redis Pub/Sub

  • 빠른 실시간 알림 전달
  • 휘발성 (영속성 없음)
  • 브로드캐스트용

Kafka

  • 메시지 영속성 보장
  • 재시도 메커니즘
  • 순서 보장
  • 복제본 3개

Worker Server

  • Kafka Consumer
  • DB 영속화 처리
  • 검색 인덱싱

핫챗 대응 전략

1. 선택적 Subscribe (부하 분산)

[핫챗 감지]
    ↓
400개 서버 중 40개만 선택
    ↓
hash(room_id) % 40
    ↓
선택된 서버만 Subscribe
나머지 360개는 UNSUBSCRIBE

효과: 서버당 부하 10배 감소

2. 배칭 (Batching)

[1초간 메시지 수집]
  5,000개 메시지 발생
    ↓
[1개의 배치 이벤트로 묶기]
  "5,000개의 새 메시지"
    ↓
[클라이언트에 1개 Push]
    ↓
[클라이언트가 한번에 Fetch]

효과: 25M Push → 5K Push (5,000배 감소)

동적 배칭 시간 조정

일반 방 (< 100 msg/sec): 1초 배칭
핫챗 (100-1000 msg/sec): 5초 배칭
극한 핫챗 (> 1000 msg/sec): 10초 배칭

3. Rate Limiting

[방별 Rate Limit]
  초당 1,000개 메시지까지만 허용
  초과분은 거부 또는 큐잉
    ↓
[Load Shedding]
  서버 CPU > 80% 시
  새 요청 거부 (503)
  기존 처리 중인 것만 완료

메시지 전달 메커니즘

TCP 방식의 Sequence 기반 전달

LINE 오픈챗 사례에서 영감을 받아, TCP의 신뢰성 있는 전달 방식을 채택했습니다.

핵심 아이디어

메시지마다 Sequence 번호 부여
  ↓
WebSocket으로 Sampling된 메시지만 Push (10%)
  ↓
클라이언트가 Gap 감지
  ↓
HTTP API로 빠진 메시지 Fetch
  ↓
순서 맞춰 화면 표시

메시지 포맷

{
  "type": "message",
  "room_id": 12345,
  "sequence": 102,
  "last_sequence": 105,
  "message": {
    "id": "msg-102",
    "text": "안녕",
    "user": "A",
    "timestamp": 1234567890
  }
}

클라이언트 Gap 처리 로직

1. WebSocket으로 수신한 메시지 저장
   received: [100, 101, 102, 105]
   last_sequence: 105

2. Gap 감지
   빠진 sequence: [103, 104]

3. HTTP Fetch API 호출
   GET /messages?sequences=103,104

4. 순서대로 화면 표시
   100 → 101 → 102 → 103 → 104 → 105

Sampling 전략

일반 상황: 100% Push
핫챗 감지: 10% Sampling
  ↓
Push 부하: 25M → 2.5M (10배 감소)
  ↓
나머지 90%는 클라이언트 Fetch

데이터베이스 전략

원자성 문제와 트레이드오프

문제 상황

[WS Server] 메시지 수신
  ↓
1. Redis Pub/Sub 발행 (실시간)
2. Kafka 발행 (영속화)
  ↓
문제:
- 이기종 시스템 간 트랜잭션 불가능
- 1번 성공, 2번 실패 시 메시지 유실
- 완벽한 원자성 보장 어려움

검토한 방안: Outbox 패턴

-- 트랜잭션으로 묶기
BEGIN TRANSACTION
  INSERT INTO messages (...)
  INSERT INTO outbox (...)  -- 이벤트 발행 예약
COMMIT

-- 별도 프로세스가 Outbox 폴링하여 발행

결정: Outbox 패턴 보류

  • 처리량 훼손 우려 (Redis Lua Script 등의 오버헤드)
  • 실시간성이 더 중요하다고 판단
  • 대신 Kafka를 통한 영속성 확보로 보완

채택한 방식: 순차 처리 + Kafka 복구

[WS Server] 메시지 수신
  ↓
1. Kafka 발행 (영속화, ACK 대기)
2. Redis Pub/Sub 발행 (실시간)
  ↓
1번 실패 시: 클라이언트에 에러 반환
1번 성공, 2번 실패 시: Kafka에 있으므로 재처리 가능

트레이드오프:

  • 완벽한 원자성 포기
  • 실시간성 우선 (50-100ms 지연)
  • 비즈니스적으로 수용 가능한 유실률 감안

Aurora 활용

Write Path

Kafka → Worker → Aurora Master

Read Path (Fetch API)

Client → HTTP API → Aurora Replica (10-15대)

캐싱 전략의 한계

"캐시는 의미 없다"

  • 핫챗에서는 동일한 메시지 조회가 반복되지 않음
  • 캐시 히트율 매우 낮음
  • DB 성능이 압도적으로 중요!

Aurora의 장점

  • Read Replica 15개까지 확장
  • Auto Scaling 지원
  • 초당 수십만 읽기 가능
  • ms 단위 복제 지연
핫챗 Fetch 부하:
  예상 수만 req/sec
    ↓
Aurora Replica 15대 분산
    ↓
Replica당 수천 req/sec
    ↓
충분히 처리 가능! ✅

트레이드오프와 결정사항

1. 실시간성 vs 안정성

선택: 안정성 우선

이유:
- 핫챗 상황에서는 메시지가 너무 빨라 읽기 불가능
- 시스템 안정성이 더 중요
- 배칭으로 10초 지연되어도 사용자 경험에 큰 영향 없음

2. Push vs Pull

선택: 하이브리드 (Push + Pull)

WebSocket Push: 빠른 알림 (10% Sampling)
HTTP Fetch: 정확한 메시지 조회 (90%)

장점:
- 서버 부하 90% 감소
- 메시지 유실 없음
- 각 채널의 장점 활용

3. 연결 유지 vs 끊기

선택: 연결 유지

Rate Limit 초과 시:
  ❌ WebSocket 연결 끊기
  ✅ 메시지만 드롭 + Fetch로 보완

이유:
- 연결 재설정 오버헤드
- 사용자 경험 저해
- 악의적 사용자만 영향

4. 배칭 시간

선택: 동적 조정

일반 방: 1초 (실시간성 유지)
핫챗: 5-10초 (안정성 우선)

"1초 텀만 줘도 2배의 공간 확보"

5. 캐싱 전략

선택: 클라이언트 로컬 캐싱

서버 캐시 (X): 히트율 낮음
클라이언트 캐시 (O):
  - 메모리 저장
  - IndexedDB 영구 저장
  - 중복 Fetch 방지
  - 오프라인 대응

주요 인사이트

LINE 오픈챗 사례에서 배운 것

  1. 핫챗은 0.1% 미만

    • 전체 시스템을 100배로 확장하는 것은 비효율
    • 핫챗만 특별 처리하는 것이 현실적
  2. 스로틀링의 중요성

    • 확률적 스로틀링 (10%)
    • 배칭 (1-10초)
    • Rate Limiting (방별 제한)
  3. 메시지 유실 방지

    • Kafka로 영속성 보장
    • Outbox 패턴으로 트랜잭션 보장
    • Sequence 기반 Gap 감지

근본 해결 vs 임시 방편

배칭: 임시 방편
  - Push 횟수만 줄임
  - 처리해야 할 메시지 수는 동일
  
Rate Limit: 근본 해결
  - 애초에 메시지 수 제한
  - 서버 보호

조합이 필요!

Pub/Sub의 한계

Redis Pub/Sub:
  ✅ 빠름
  ❌ 휘발성 (유실 가능)
  ❌ 모든 구독자에게 브로드캐스트 (부하 분산 불가)

해결:
  - Kafka 병행 (영속성)
  - 선택적 Subscribe (부하 분산)
  - Sequence 기반 Fetch (유실 복구)

미해결 과제

1. Fetch API 부하 관리

문제:
- 10,000개 방 × Sampling 10% = 대량 Fetch 발생 가능
- 동시에 여러 방에서 핫챗 발생 시 DB 부하 폭증

검토 중인 방안:
- Request Coalescing: 0.5-3초간 동일 요청 모아서 처리
- 클라이언트 로컬 캐싱 최대화
- Aurora Replica 확장

2. 쓰기/읽기 분리 아키텍처

현재: WebSocket으로 읽기 + 쓰기
제안: POST API로 쓰기, WebSocket은 읽기 전용

장점:
- 역할 분리 명확
- Rate Limit 구현 용이
- 확장성 향상

검토 필요:
- 클라이언트 복잡도 증가
- 두 채널 관리 오버헤드

3. Redis 클러스터링 성능

의문:
- 100개 노드로 분산해도 실시간성 보장 가능한가?
- Sharded Pub/Sub 활용 방안?

추가 검증 필요

4. 동시 입장 시나리오 통합

QR 2,000명 동시 입장:
- MySQL Auto-increment lock 경합
- Rate Limiting 필요
- 비동기 처리 고려

다음 세션에서 논의 예정

예상 인프라 비용

WebSocket 서버: 400대 × $500/월 = $200K
Aurora Master/Replica: $100K/월
Kafka Cluster: $50K/월
Redis Cluster: $50K/월
네트워크 및 기타: $50K/월
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
총: $450K/월 (약 5.4억원/월) (실화?)

결론

대규모 채팅 시스템 설계의 핵심은 완벽한 실시간성보다 안정성과 확장성입니다.

핵심 원칙

  1. 핫챗을 정상이 아닌 예외로 취급
  2. Rate Limiting으로 입구 제어
  3. 배칭으로 부하 완충
  4. Sequence 기반으로 신뢰성 보장
  5. DB 성능이 캐싱보다 중요

최종 아키텍처 요약

  • WebSocket: 빠른 알림 (Sampling 10%)
  • HTTP API: 정확한 조회 (Gap Fetch)
  • Kafka: 영속성 보장
  • Aurora: 고성능 읽기
  • 동적 배칭: 상황별 조정

Action Items

  • 다음 세션에서 쓰기/읽기 분리 아키텍처 검토
  • 동시 입장 시나리오 통합 설계
  • Request Coalescing 효과 검증
  • Redis Sharded Pub/Sub 성능 테스트

회고

설계 과정에서 배운 점

  1. 병목은 Subscribe에 있었다

    • Publish는 빠르지만 400개 서버가 모두 Subscribe
    • 각 서버의 Push 처리가 진짜 문제
  2. 완벽한 원자성은 어렵다

    • 이기종 시스템 간 트랜잭션 불가능
    • 실시간성과 신뢰성 사이의 트레이드오프
    • 비즈니스적 허용 범위 내에서 타협
  3. 캐싱이 항상 답은 아니다

    • 핫챗에서는 캐시 히트율이 매우 낮음
    • DB 성능 자체가 더 중요
    • 클라이언트 로컬 캐싱이 더 효과적
  4. 배칭은 근본 해결이 아니다

    • Push 횟수만 줄일 뿐
    • Rate Limiting으로 입구 제어가 근본
    • 하지만 조합 시 큰 효과
  5. 실시간성 vs 안정성

    • 핫챗에서는 실시간이 의미 없음 (읽을 수 없을 정도로 빠름)
    • 시스템 안정성이 우선
    • 10초 배칭도 사용자 경험에 큰 영향 없음

참고 자료


작성일: 2026년 3월 5일
시스템 디자인 세션 #3 - 오픈채팅 시스템

profile
경험과 삽질, 모든 것을 기록합니다.

1개의 댓글

comment-user-thumbnail
2026년 3월 10일

좋은정보 감사합니다. 이해할때까지 노력해보겠습니다/

답글 달기