실전 설계 예제 2 (Practical Design Examples 2)
실전 설계 예제 2 (Practical Design Examples 2)¶
난이도: ⭐⭐⭐⭐
개요¶
이 장에서는 소셜 미디어와 실시간 통신 시스템을 설계합니다: 뉴스 피드/타임라인, 채팅 시스템, 알림 시스템. 이러한 시스템은 대규모 사용자를 다루며, 실시간성과 확장성이 중요한 과제입니다.
목차¶
1. 뉴스 피드 / 타임라인¶
1.1 요구사항 정의¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 기능 요구사항 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 핵심 기능: │
│ 1. 포스트 작성: 텍스트, 이미지, 비디오 │
│ 2. 뉴스 피드 조회: 팔로우하는 사용자의 포스트 │
│ 3. 타임라인 조회: 특정 사용자의 포스트 목록 │
│ │
│ 부가 기능: │
│ 4. 좋아요, 댓글 │
│ 5. 무한 스크롤 │
│ 6. 실시간 업데이트 │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ 규모 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ - 일간 활성 사용자(DAU): 100M │
│ - 사용자당 팔로우: 평균 200명 │
│ - 일간 포스트: 10M │
│ - 피드 조회 빈도: 10회/일/사용자 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
1.2 핵심 과제: Push vs Pull¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 피드 생성 전략 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 방법 1: Pull (Fan-out on Read) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 사용자가 피드 요청 시: │ │
│ │ │ │
│ │ 1. 팔로우 목록 조회 │ │
│ │ 2. 각 팔로우의 최신 포스트 조회 │ │
│ │ 3. 병합 및 정렬 │ │
│ │ 4. 반환 │ │
│ │ │ │
│ │ ┌──────────┐ │ │
│ │ │ User │─── GET /feed │ │
│ │ └────┬─────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ SELECT * FROM posts │ │ │
│ │ │ WHERE author_id IN (SELECT followee FROM follows │ │ │
│ │ │ WHERE follower = user_id) │ │ │
│ │ │ ORDER BY created_at DESC │ │ │
│ │ │ LIMIT 20; │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 장점: │ │
│ │ - 저장 공간 절약 │ │
│ │ - 포스트 작성이 빠름 │ │
│ │ │ │
│ │ 단점: │ │
│ │ - 피드 조회가 느림 (팔로우 많으면) │ │
│ │ - DB 부하 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 방법 2: Push (Fan-out on Write) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 포스트 작성 시: │ │
│ │ │ │
│ │ 1. 포스트 저장 │ │
│ │ 2. 모든 팔로워의 피드에 추가 │ │
│ │ │ │
│ │ ┌──────────┐ │ │
│ │ │ Author │─── POST /posts │ │
│ │ └────┬─────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ Save Post │ │ │
│ │ └──────┬───────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ ┌──────────────────────────────────────┐ │ │
│ │ │ Fan-out to │────►│ Follower 1 Feed: [post_id, ...] │ │ │
│ │ │ all followers│────►│ Follower 2 Feed: [post_id, ...] │ │ │
│ │ │ │────►│ Follower 3 Feed: [post_id, ...] │ │ │
│ │ │ │────►│ ... │ │ │
│ │ └──────────────┘ └──────────────────────────────────────┘ │ │
│ │ │ │
│ │ 장점: │ │
│ │ - 피드 조회가 빠름 (사전 계산됨) │ │
│ │ │ │
│ │ 단점: │ │
│ │ - 저장 공간 많이 사용 │ │
│ │ - 셀럽(팔로워 많은 사용자) 포스트 지연 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
1.3 하이브리드 접근¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 하이브리드 팬아웃 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 핵심 아이디어: │
│ - 일반 사용자: Push (Fan-out on Write) │
│ - 핫 유저(셀럽): Pull (Fan-out on Read) │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 포스트 작성: │ │
│ │ │ │
│ │ ┌──────────┐ │ │
│ │ │ Author │──► 팔로워 수 확인 │ │
│ │ └────┬─────┘ │ │
│ │ │ │ │
│ │ ┌────┴────┐ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ < 10K ≥ 10K │ │
│ │ 팔로워 팔로워 │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ Push Save to │ │
│ │ to all hot_posts │ │
│ │ feeds table only │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 피드 조회: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 사전 계산된 피드 조회 (Push된 포스트) │ │
│ │ 2. 팔로우 중인 핫 유저의 최신 포스트 조회 │ │
│ │ 3. 병합 및 정렬 │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ Pre-computed Feed Hot Users Posts │ │ │
│ │ │ ┌───────────────┐ ┌───────────────┐ │ │ │
│ │ │ │ [post1] │ │ [celeb_post1] │ │ │ │
│ │ │ │ [post2] │ + │ [celeb_post2] │ │ │ │
│ │ │ │ [post3] │ │ │ │ │ │
│ │ │ └───────────────┘ └───────────────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └────────┬───────────┘ │ │ │
│ │ │ ▼ │ │ │
│ │ │ Merge & Sort │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ Final Feed │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
1.4 시스템 아키텍처¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 뉴스 피드 아키텍처 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌────────┐ │ │
│ │ │ Client │ │ │
│ │ └───┬────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ API Gateway │ │ │
│ │ └───────┬─────────┘ │ │
│ │ │ │ │
│ │ ┌───────┴───────┐ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Post Service│ │ Feed Service│ │ │
│ │ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │
│ │ ▼ │ │ │
│ │ ┌─────────────┐ │ │ │
│ │ │ Posts DB │ │ │ │
│ │ │ (Sharded) │ │ │ │
│ │ └──────┬──────┘ │ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Message Queue │ │ │
│ │ │ (Kafka) │ │ │
│ │ └─────────────────┬────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Fanout Workers │ │ │
│ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │
│ │ │ │Worker 1│ │Worker 2│ │Worker 3│ │ │ │
│ │ │ └───┬────┘ └───┬────┘ └───┬────┘ │ │ │
│ │ └──────┼──────────┼──────────┼────────┘ │ │
│ │ │ │ │ │ │
│ │ └──────────┼──────────┘ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Feed Cache (Redis) │ │ │
│ │ │ user:123:feed = [post_ids...] │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
1.5 피드 캐싱 전략¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 피드 캐싱 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Redis 피드 구조: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Key: feed:{user_id} │ │
│ │ Type: Sorted Set │ │
│ │ Score: timestamp │ │
│ │ Member: post_id │ │
│ │ │ │
│ │ ZADD feed:123 1704067200 "post_abc" │ │
│ │ ZADD feed:123 1704067300 "post_def" │ │
│ │ ... │ │
│ │ │ │
│ │ 조회: ZREVRANGE feed:123 0 19 │ │
│ │ → 최신 20개 포스트 ID │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 캐시 관리: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ - 피드 크기 제한: 최근 800개 포스트만 유지 │ │
│ │ ZREMRANGEBYRANK feed:123 0 -801 │ │
│ │ │ │
│ │ - TTL 설정: 활성 사용자만 캐시 유지 │ │
│ │ 비활성 사용자 → 조회 시 재구축 │ │
│ │ │ │
│ │ - 포스트 내용은 별도 캐시 │ │
│ │ Key: post:{post_id} │ │
│ │ Value: { author, content, media, ... } │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2. 채팅 시스템¶
2.1 요구사항 정의¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 기능 요구사항 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 핵심 기능: │
│ 1. 1:1 채팅 │
│ 2. 그룹 채팅 (최대 500명) │
│ 3. 온라인 상태 표시 │
│ 4. 읽음 확인 │
│ │
│ 메시지 기능: │
│ 5. 텍스트, 이미지, 파일 전송 │
│ 6. 메시지 히스토리 동기화 │
│ 7. 푸시 알림 (오프라인 시) │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ 비기능 요구사항 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ - 실시간 전송: < 100ms │
│ - 메시지 순서 보장 │
│ - 메시지 유실 방지 │
│ - 동시 접속: 수백만 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.2 통신 프로토콜¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 프로토콜 선택 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. HTTP Polling │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Client Server │ │
│ │ │ │ │ │
│ │ │── GET /messages ─►│ │ │
│ │ │◄── [] ────────────│ │ │
│ │ │ │ │ │
│ │ │── GET /messages ─►│ (5초 후) │ │
│ │ │◄── [] ────────────│ │ │
│ │ │ │ │ │
│ │ │── GET /messages ─►│ (5초 후) │ │
│ │ │◄── [msg1] ────────│ │ │
│ │ │ │
│ │ 단점: 지연, 불필요한 요청, 서버 부하 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 2. Long Polling │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Client Server │ │
│ │ │ │ │ │
│ │ │── GET /messages ─►│ │ │
│ │ │ (대기...) │ │ │
│ │ │ │ 메시지 도착! │ │
│ │ │◄── [msg1] ────────│ │ │
│ │ │── GET /messages ─►│ (즉시 재연결) │ │
│ │ │ (대기...) │ │ │
│ │ │ │
│ │ 개선: 불필요한 요청 감소 │ │
│ │ 단점: 연결 오버헤드, 서버 리소스 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 3. WebSocket (권장) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Client Server │ │
│ │ │ │ │ │
│ │ │── HTTP Upgrade ──►│ │ │
│ │ │◄─ 101 Switching ──│ │ │
│ │ │ │ │ │
│ │ │═══ WebSocket ═════│ (양방향 연결 유지) │ │
│ │ │ │ │ │
│ │ │◄── [msg1] ────────│ (서버 푸시) │ │
│ │ │── [msg2] ────────►│ (클라이언트 전송) │ │
│ │ │◄── [msg3] ────────│ │ │
│ │ │ │
│ │ 장점: 실시간, 양방향, 오버헤드 최소 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.3 시스템 아키텍처¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 채팅 시스템 아키텍처 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Client A │ │ Client B │ │ Client C │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ WebSocket │ WebSocket │ WebSocket │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Load Balancer │ │ │
│ │ │ (Sticky Sessions by user_id) │ │ │
│ │ └──────────────────────┬──────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────────────────┼─────────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Chat Srv 1 │ │ Chat Srv 2 │ │ Chat Srv 3 │ │ │
│ │ │ [A's conn] │ │ [B's conn] │ │ [C's conn] │ │ │
│ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │
│ │ │ │ │ │ │
│ │ └────────────────┼────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Message Broker (Redis Pub/Sub) │ │ │
│ │ │ │ │ │
│ │ │ A sends to B: │ │ │
│ │ │ 1. Chat Srv 1 → PUBLISH chat:B "msg from A" │ │ │
│ │ │ 2. Chat Srv 2 ← SUBSCRIBE chat:B → deliver to B │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Message Store (Cassandra) │ │ │
│ │ │ │ │ │
│ │ │ Partition Key: conversation_id │ │ │
│ │ │ Clustering Key: message_id (time-based) │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.4 메시지 전송 흐름¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 1:1 메시지 전송 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ User A → User B 메시지 전송: │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. A ─── [msg] ───► Chat Server 1 │ │
│ │ │ │ │
│ │ 2. ▼ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Message Service │ │ │
│ │ │ - Generate message_id │ │ │
│ │ │ - Validate message │ │ │
│ │ └─────────────┬───────────────┘ │ │
│ │ │ │ │
│ │ 3. │ │ │
│ │ ┌─────────────┼───────────────┐ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ Store │ │ Publish │ │ Check B │ │ │
│ │ │ to DB │ │ to Kafka │ │ online? │ │ │
│ │ └──────────┘ └──────────┘ └──────┬───────┘ │ │
│ │ │ │ │
│ │ 4. ┌──────┴──────┐ │ │
│ │ │ │ │ │
│ │ Online Offline │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Pub/Sub │ │ Push │ │ │
│ │ │ to B │ │ Queue │ │ │
│ │ └────┬─────┘ └──────────┘ │ │
│ │ │ │ │
│ │ 5. ▼ │ │
│ │ Chat Server 2 ─── [msg] ───► B │ │
│ │ │ │
│ │ 6. A ◄─── [ack] ─── Chat Server 1 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.5 그룹 채팅¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 그룹 채팅 설계 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 그룹 메시지 전송: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. Sender ─── [msg] ───► Message Service │ │
│ │ │ │ │
│ │ 2. ▼ │ │
│ │ Get group members (100명) │ │
│ │ │ │ │
│ │ 3. ▼ │ │
│ │ ┌────────────────┼────────────────┐ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ [Online 60명] [Online 30명] [Offline 10명] │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ Pub/Sub Pub/Sub Push Queue │ │
│ │ (batch) (batch) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 최적화: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 온라인 멤버 우선 전송 │ │
│ │ 2. 배치 처리 (한 번에 여러 멤버) │ │
│ │ 3. 읽음 확인은 샘플링 (전체 X) │ │
│ │ 4. 대형 그룹: 클라이언트 Pull 방식 고려 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.6 온라인 상태 관리¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 온라인 상태 (Presence) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 상태 종류: │
│ - Online: 현재 연결됨 │
│ - Offline: 연결 없음 │
│ - Away: 연결됐지만 비활성 │
│ - Last Seen: 마지막 접속 시간 │
│ │
│ 구현: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. Heartbeat 기반 │ │
│ │ │ │
│ │ Client ─── heartbeat (30초마다) ───► Server │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Redis: SET presence:user123 "online"│ │
│ │ EXPIRE presence:user123 60 │ │
│ │ │ │
│ │ 60초간 heartbeat 없으면 → 키 만료 → Offline │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 상태 전파: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ User A의 상태 변경 시: │ │
│ │ │ │
│ │ 1. A의 친구 목록 조회 │ │
│ │ 2. 현재 온라인인 친구만 필터링 │ │
│ │ 3. 해당 친구들에게 상태 변경 푸시 │ │
│ │ │ │
│ │ 최적화: │ │
│ │ - 친구가 많으면 배치 처리 │ │
│ │ - 빈번한 변경 방지 (debounce) │ │
│ │ - 활성 대화창의 상대방에게만 우선 전파 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.7 메시지 저장¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 메시지 DB 스키마 (Cassandra) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ messages 테이블: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ CREATE TABLE messages ( │ │
│ │ conversation_id UUID, -- 대화방 ID │ │
│ │ message_id TIMEUUID, -- 시간 기반 UUID │ │
│ │ sender_id UUID, -- 발신자 │ │
│ │ content TEXT, -- 메시지 내용 │ │
│ │ content_type TEXT, -- text, image, file │ │
│ │ created_at TIMESTAMP, -- 생성 시간 │ │
│ │ PRIMARY KEY (conversation_id, message_id) │ │
│ │ ) WITH CLUSTERING ORDER BY (message_id DESC); │ │
│ │ │ │
│ │ -- 최신 메시지부터 정렬 │ │
│ │ -- 대화방별 파티셔닝 (핫 파티션 주의) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ conversations 테이블: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ CREATE TABLE user_conversations ( │ │
│ │ user_id UUID, │ │
│ │ last_message_at TIMESTAMP, -- 정렬용 │ │
│ │ conversation_id UUID, │ │
│ │ conversation_type TEXT, -- dm, group │ │
│ │ unread_count INT, -- 읽지 않은 메시지 │ │
│ │ PRIMARY KEY (user_id, last_message_at, conversation_id) │ │
│ │ ) WITH CLUSTERING ORDER BY (last_message_at DESC); │ │
│ │ │ │
│ │ -- 사용자별 대화 목록 조회 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
3. 알림 시스템¶
3.1 요구사항 정의¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 기능 요구사항 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 알림 채널: │
│ 1. iOS 푸시 (APNs) │
│ 2. Android 푸시 (FCM) │
│ 3. SMS │
│ 4. Email │
│ │
│ 기능: │
│ 5. 알림 템플릿 │
│ 6. 사용자별 알림 설정 │
│ 7. 스케줄 알림 │
│ 8. 알림 히스토리 │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ 비기능 요구사항 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ - 일간 알림: 10B (100억) │
│ - Soft real-time: 수 초 내 전송 │
│ - 중복 방지 │
│ - 우선순위 지원 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
3.2 시스템 아키텍처¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 알림 시스템 아키텍처 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Event Sources │ │ │
│ │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │
│ │ │ │Order │ │Payment │ │Social │ │Schedule│ │ │ │
│ │ │ │Service │ │Service │ │Service │ │Service │ │ │ │
│ │ │ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ └──────┼──────────┼──────────┼──────────┼────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ └──────────┴──────────┴──────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Notification Service │ │ │
│ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 1. Validation │ │ │ │
│ │ │ │ 2. User Preferences Check │ │ │ │
│ │ │ │ 3. Rate Limiting │ │ │ │
│ │ │ │ 4. Template Rendering │ │ │ │
│ │ │ │ 5. Priority Assignment │ │ │ │
│ │ │ └──────────────────────────────────────────────────────┘ │ │ │
│ │ └──────────────────────┬──────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Message Queues (Priority-based) │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │
│ │ │ │ High Queue │ │ Medium Queue│ │ Low Queue │ │ │ │
│ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │
│ │ └─────────┼───────────────┼───────────────┼───────────────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Workers │ │ │
│ │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │
│ │ │ │ iOS │ │Android │ │ SMS │ │ Email │ │ │ │
│ │ │ │ Worker │ │ Worker │ │ Worker │ │ Worker │ │ │ │
│ │ │ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ │ │ │
│ │ └──────┼──────────┼──────────┼──────────┼─────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ APNs │ │ FCM │ │Twilio│ │ SES │ │ │
│ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
3.3 알림 흐름¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 알림 전송 흐름 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 이벤트 수신 │ │
│ │ ───────────── │ │
│ │ { │ │
│ │ "event_type": "order_shipped", │ │
│ │ "user_id": "user123", │ │
│ │ "data": { "order_id": "ORD456", "tracking": "..." } │ │
│ │ } │ │
│ │ │ │
│ │ 2. 사용자 설정 확인 │ │
│ │ ───────────────── │ │
│ │ SELECT * FROM user_notification_settings │ │
│ │ WHERE user_id = 'user123'; │ │
│ │ │ │
│ │ → push: true, email: true, sms: false │ │
│ │ │ │
│ │ 3. 디바이스 토큰 조회 │ │
│ │ ───────────────── │ │
│ │ SELECT device_token, platform │ │
│ │ FROM user_devices WHERE user_id = 'user123'; │ │
│ │ │ │
│ │ → [{ token: "abc...", platform: "ios" }, │ │
│ │ { token: "def...", platform: "android" }] │ │
│ │ │ │
│ │ 4. 템플릿 렌더링 │ │
│ │ ───────────────── │ │
│ │ Template: "Your order {order_id} has been shipped!" │ │
│ │ Result: "Your order ORD456 has been shipped!" │ │
│ │ │ │
│ │ 5. 중복 체크 │ │
│ │ ───────────────── │ │
│ │ Redis SETNX dedup:{event_hash} 1 EX 86400 │ │
│ │ → 24시간 내 동일 알림 방지 │ │
│ │ │ │
│ │ 6. 큐에 발행 │ │
│ │ ───────────────── │ │
│ │ Publish to ios_queue, android_queue, email_queue │ │
│ │ │ │
│ │ 7. Worker 처리 │ │
│ │ ───────────────── │ │
│ │ iOS Worker → APNs API → Apple 서버 │ │
│ │ Android Worker → FCM API → Google 서버 │ │
│ │ Email Worker → SES API → 이메일 전송 │ │
│ │ │ │
│ │ 8. 결과 저장 │ │
│ │ ───────────────── │ │
│ │ INSERT INTO notification_logs (...) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
3.4 중복 방지¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 중복 알림 방지 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 문제: │
│ - 이벤트 중복 발행 │
│ - Worker 재시도로 인한 중복 │
│ - 분산 환경에서의 Race Condition │
│ │
│ 해결책: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 이벤트 레벨 중복 제거 │ │
│ │ ───────────────────────── │ │
│ │ event_key = hash(event_type + user_id + key_data) │ │
│ │ │ │
│ │ if not redis.setnx(f"dedup:{event_key}", 1, ex=86400): │ │
│ │ return # 이미 처리됨 │ │
│ │ │ │
│ │ 2. 전송 레벨 중복 제거 │ │
│ │ ───────────────────────── │ │
│ │ notification_id = generate_unique_id() │ │
│ │ │ │
│ │ 각 채널별 전송 전 체크: │ │
│ │ if not redis.setnx(f"sent:{notification_id}:{channel}", 1): │ │
│ │ return # 이미 전송됨 │ │
│ │ │ │
│ │ 3. 빈도 제한 │ │
│ │ ───────────────────────── │ │
│ │ max_per_hour = 10 │ │
│ │ │ │
│ │ current = redis.incr(f"rate:{user_id}:{hour}") │ │
│ │ if current > max_per_hour: │ │
│ │ # 제한 초과 → 나중에 발송 또는 무시 │ │
│ │ enqueue_for_later() │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
3.5 우선순위 처리¶
┌─────────────────────────────────────────────────────────────────────────┐
│ 알림 우선순위 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 우선순위 분류: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ HIGH (즉시): │ │
│ │ - 보안 알림 (비정상 로그인) │ │
│ │ - 2FA 인증 코드 │ │
│ │ - 긴급 시스템 알림 │ │
│ │ │ │
│ │ MEDIUM (수 초 내): │ │
│ │ - 채팅 메시지 │ │
│ │ - 주문 상태 변경 │ │
│ │ - 결제 알림 │ │
│ │ │ │
│ │ LOW (수 분 ~ 시간): │ │
│ │ - 마케팅 알림 │ │
│ │ - 추천 알림 │ │
│ │ - 요약 알림 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 구현: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 우선순위별 큐: │ │
│ │ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ HIGH Queue │ ──► 전용 Worker 10대 │ │
│ │ └─────────────────┘ (항상 즉시 처리) │ │
│ │ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ MEDIUM Queue │ ──► 공유 Worker 50대 │ │
│ │ └─────────────────┘ (HIGH 비어있을 때) │ │
│ │ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ LOW Queue │ ──► 공유 Worker 50대 │ │
│ │ └─────────────────┘ (HIGH, MEDIUM 비어있을 때) │ │
│ │ │ │
│ │ Worker 처리 순서: │ │
│ │ while True: │ │
│ │ msg = high_queue.pop() or │ │
│ │ medium_queue.pop() or │ │
│ │ low_queue.pop() │ │
│ │ process(msg) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
3.6 알림 설정 스키마¶
┌─────────────────────────────────────────────────────────────────────────┐
│ DB 스키마 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ user_notification_settings: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ user_id UUID PK │ │
│ │ channel VARCHAR PK (push/email/sms) │ │
│ │ enabled BOOLEAN │ │
│ │ quiet_hours JSONB {"start": "22:00", "end": "08:00"}│ │
│ │ frequency VARCHAR immediate/daily/weekly │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ notification_type_settings: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ user_id UUID PK │ │
│ │ notification_type VARCHAR PK (order/social/marketing) │ │
│ │ push_enabled BOOLEAN │ │
│ │ email_enabled BOOLEAN │ │
│ │ sms_enabled BOOLEAN │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ user_devices: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ device_id UUID PK │ │
│ │ user_id UUID FK │ │
│ │ platform VARCHAR ios/android │ │
│ │ device_token VARCHAR 푸시 토큰 │ │
│ │ last_active TIMESTAMP 마지막 활성 │ │
│ │ app_version VARCHAR 앱 버전 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ notification_logs: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ id UUID PK │ │
│ │ user_id UUID │ │
│ │ type VARCHAR 알림 유형 │ │
│ │ channel VARCHAR 전송 채널 │ │
│ │ content JSONB 알림 내용 │ │
│ │ status VARCHAR sent/failed/pending │ │
│ │ sent_at TIMESTAMP 전송 시간 │ │
│ │ read_at TIMESTAMP 읽음 시간 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
4. 연습 문제¶
연습 1: 뉴스 피드 확장¶
다음 기능을 추가하는 설계를 하세요: - 광고 삽입 (5번째 포스트마다) - 트렌딩 포스트 추천 - "관심 없음" 피드백 반영
연습 2: 채팅 시스템 확장¶
다음 요구사항을 만족하는 설계를 하세요: - End-to-End 암호화 - 메시지 편집/삭제 (24시간 내) - 화상/음성 통화 시그널링
연습 3: 알림 시스템 최적화¶
다음 상황을 처리하는 설계를 하세요: - 글로벌 서비스: 다국어 알림 - 알림 배칭: 유사 알림 묶음 - A/B 테스트: 알림 문구 최적화
마무리¶
이 시리즈를 통해 시스템 설계의 핵심 개념과 패턴을 학습했습니다. 실제 면접이나 프로젝트에서는 요구사항을 명확히 하고, 트레이드오프를 고려하며, 확장 가능한 설계를 하는 것이 중요합니다.
다음 단계로는: - 실제 오픈소스 시스템 코드 분석 - 기업 기술 블로그 연구 (Netflix, Uber, Twitter 등) - 모의 시스템 설계 면접 연습
참고 자료¶
- "System Design Interview" - Alex Xu Vol.1 & Vol.2
- "Designing Data-Intensive Applications" - Martin Kleppmann
- Twitter Timeline Architecture
- Facebook News Feed Architecture
- WhatsApp Architecture at Scale
- Discord How Discord Stores Billions of Messages
- Airbnb Notification System