Skip to content
Published on

시스템 디자인 면접 완전 가이드 2025: 대규모 시스템 설계, 확장성, 가용성 패턴

Authors

서론

시스템 디자인 면접은 시니어 엔지니어 채용에서 가장 중요한 평가 항목입니다. 알고리즘 문제가 코딩 능력을 측정한다면, 시스템 디자인은 대규모 시스템을 설계하고 트레이드오프를 이해하는 종합적인 엔지니어링 역량을 평가합니다.

Google, Meta, Amazon, Netflix 등 빅테크 기업들은 4560분 동안 수백만수억 명의 사용자를 처리할 수 있는 시스템을 설계하도록 요구합니다. 이 면접에서 성공하려면 단순히 기술을 아는 것이 아니라, 요구사항을 분석하고 적절한 기술을 선택하며 트레이드오프를 명확히 설명할 수 있어야 합니다.

이 가이드에서는 시스템 디자인 면접의 4단계 프레임워크부터 확장성, 캐싱, 데이터베이스, 메시지 큐, 실전 설계 예제까지 체계적으로 다룹니다.


1. 시스템 디자인 면접 4단계 프레임워크

1.1 1단계: 요구사항 명확화 (5분)

면접에서 가장 중요한 첫 단계입니다. 바로 설계에 들어가는 것은 가장 흔한 실수입니다.

기능적 요구사항 (Functional Requirements):

  • 핵심 기능은 무엇인가?
  • 사용자가 수행할 주요 작업은?
  • 입력과 출력은 무엇인가?

비기능적 요구사항 (Non-Functional Requirements):

  • 예상 사용자 수 / DAU
  • 읽기 대 쓰기 비율
  • 지연 시간 요구사항
  • 가용성 요구 수준 (99.9%? 99.99%?)
  • 데이터 일관성 요구 수준
면접관: "URL 단축 서비스를 설계해 주세요."

좋은 질문 예시:
Q: "일간 활성 사용자(DAU)는 어느 정도인가요?"
A: "1억 명입니다."

Q: "URL 단축과 리디렉션 중 어느 것이 더 중요한가요?"
A: "리디렉션이 더 중요합니다. 읽기가 훨씬 많습니다."

Q: "단축 URL의 길이에 제한이 있나요?"
A: "가능한 한 짧게요."

Q: "커스텀 URL을 지원해야 하나요?"
A: "네, 선택적으로 지원해 주세요."

1.2 2단계: 상위 수준 설계 (15~20분)

요구사항을 바탕으로 전체 시스템의 큰 그림을 그립니다.

┌──────────┐     ┌──────────────┐     ┌──────────────┐
Client  │────▶│ Load Balancer│────▶│  Web Server└──────────┘     └──────────────┘     └──────┬───────┘
                    ┌────────────────────────┤
                    ▼                        ▼
             ┌──────────┐            ┌──────────────┐
Cache   │            │  Database             └──────────┘            └──────────────┘

이 단계에서 다뤄야 할 것:

  • API 설계 (RESTful 엔드포인트)
  • 데이터 모델 설계
  • 주요 컴포넌트와 상호 관계
  • 데이터 흐름

1.3 3단계: 상세 설계 (15~20분)

면접관이 관심 있는 특정 컴포넌트를 깊이 파고듭니다.

  • 데이터베이스 스키마와 인덱싱 전략
  • 캐싱 전략과 무효화
  • 확장성 병목 지점과 해결 방안
  • 장애 처리와 복구 방안

1.4 4단계: 마무리 (5분)

  • 설계의 병목 지점 식별
  • 향후 확장 방향 논의
  • 모니터링/알림 전략
  • 추가 기능 제안

2. 확장성 (Scalability)

2.1 수직 확장 (Vertical Scaling)

기존 서버의 하드웨어 사양을 올리는 방식입니다.

수직 확장 전:           수직 확장 후:
┌─────────────┐        ┌─────────────┐
4 CPU      │        │  32 CPU│  16GB RAM   │   ──▶  │  256GB RAM│  500GB SSD  │        │  4TB SSD└─────────────┘        └─────────────┘

장점: 구현이 간단, 데이터 일관성 유지 쉬움, 소프트웨어 변경 불필요

단점: 물리적 한계 존재 (단일 서버의 성능 상한), 단일 장애 지점(SPOF), 비용이 지수적으로 증가

2.2 수평 확장 (Horizontal Scaling)

서버 수를 늘려 부하를 분산하는 방식입니다.

수평 확장:
                    ┌─────────────┐
              ┌────▶│  Server 1              │     └─────────────┘
┌──────────┐  │     ┌─────────────┐
Load   │──┼────▶│  Server 2Balancer │  │     └─────────────┘
└──────────┘  │     ┌─────────────┐
              ├────▶│  Server 3              │     └─────────────┘
              │     ┌─────────────┐
              └────▶│  Server N                    └─────────────┘

장점: 이론적으로 무제한 확장 가능, 장애 격리, 점진적 확장

단점: 데이터 일관성 관리 복잡, 세션 관리 필요, 운영 복잡도 증가

2.3 Stateless 서비스 설계

수평 확장의 핵심은 서버를 Stateless로 만드는 것입니다.

Bad (Stateful):
┌──────────┐  session  ┌──────────┐
Client  │──────────▶│ Server A   (Server A가 죽으면 세션 유실)
└──────────┘           └──────────┘

Good (Stateless):
┌──────────┐     ┌──────────┐     ┌──────────┐
Client  │────▶│   Any    │────▶│  Redis└──────────┘     │  Server (Session)                 └──────────┘     └──────────┘

세션, 캐시, 상태 정보를 외부 저장소(Redis, Memcached)에 보관하면 어떤 서버든 동일하게 요청을 처리할 수 있습니다.

2.4 Auto-Scaling 전략

# AWS Auto Scaling 설정 예시
AutoScalingGroup:
  MinSize: 2
  MaxSize: 20
  DesiredCapacity: 4
  TargetTrackingScaling:
    TargetValue: 70.0          # CPU 70% 유지
    PredefinedMetricType: ASGAverageCPUUtilization

  # 스케줄 기반 스케일링 (예측 가능한 트래픽)
  ScheduledActions:
    - Schedule: "0 8 * * MON-FRI"   # 평일 오전 8시
      MinSize: 6
    - Schedule: "0 22 * * *"        # 매일 오후 10시
      MinSize: 2
전략기반적합한 상황
Target Tracking메트릭 목표값CPU, 요청 수 기반
Step Scaling임계값 단계급격한 트래픽 변화
Scheduled시간 기반예측 가능한 패턴
PredictiveML 기반반복적 패턴

3. 로드 밸런싱 (Load Balancing)

3.1 로드 밸런싱 알고리즘

1. Round Robin (순차 배분)
   Request 1Server A
   Request 2Server B
   Request 3Server C
   Request 4Server A  (다시 처음부터)

2. Weighted Round Robin (가중 순차)
   Server A (weight=5): 5개 요청 처리
   Server B (weight=3): 3개 요청 처리
   Server C (weight=2): 2개 요청 처리

3. Least Connections (최소 연결)
   Server A: 10 connections ← 새 요청은 여기로
   Server B: 25 connections
   Server C: 18 connections

4. IP Hash (IP 기반 분배)
   hash(client_ip) % server_count = target_server
   → 같은 클라이언트는 항상 같은 서버로

3.2 L4 vs L7 로드 밸런서

L4 (전송 계층):
┌────────┐    TCP/UDP     ┌──────────┐
Client │──────────────▶│ L4 LB    │── IP:Port 기반 분배
└────────┘               └──────────┘
  속도 빠름, 단순함, SSL 오프로드 불가

L7 (애플리케이션 계층):
┌────────┐    HTTP/HTTPS   ┌──────────┐
Client │──────────────▶│ L7 LB    │── URL, Header, Cookie 기반 분배
└────────┘               └──────────┘
  콘텐츠 기반 라우팅, SSL 종료, 더 유연함
특성L4L7
계층TCP/UDPHTTP/HTTPS
성능매우 빠름상대적으로 느림
라우팅IP/PortURL, Header, Cookie
SSL 종료불가가능
사용 예AWS NLBAWS ALB, Nginx

3.3 Consistent Hashing

일반적인 해싱은 서버가 추가/제거될 때 거의 모든 키가 재배치됩니다. Consistent Hashing은 이 문제를 해결합니다.

일반 해싱:
hash(key) % 3 = 서버 번호
→ 서버 추가시: hash(key) % 4 = 대부분 다른 서버로!

Consistent Hashing (해시 링):

        Server A
        /      \
       /   0    \
      /          \
  Server D     Server B
      \          /
       \   key  /
        \  ↓  /
        Server C

→ 서버 추가시: 인접한 키만 이동 (전체의 약 1/N)

가상 노드(Virtual Nodes)로 균등 분배:

class ConsistentHash:
    def __init__(self, nodes, virtual_nodes=150):
        self.ring = SortedDict()
        self.virtual_nodes = virtual_nodes

        for node in nodes:
            for i in range(virtual_nodes):
                key = self._hash(f"node-vn-i")
                self.ring[key] = node

    def get_node(self, data_key):
        if not self.ring:
            return None
        hash_val = self._hash(data_key)
        # 해시 링에서 시계 방향으로 가장 가까운 노드
        idx = self.ring.bisect_right(hash_val)
        if idx == len(self.ring):
            idx = 0
        return self.ring.peekitem(idx)[1]

    def _hash(self, key):
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

4. 캐싱 (Caching)

4.1 캐싱 전략

1. Cache-Aside (Lazy Loading):
   ┌────────┐  1.읽기  ┌───────┐
App   │────────▶│ Cache   └────┬───┘  miss   └───────┘
2.DB 읽기      ▲
3.캐시 저장
   ┌────────┐
DB   └────────┘

2. Write-Through:
   ┌────────┐  1.쓰기  ┌───────┐  2.동기 쓰기  ┌────────┐
App   │────────▶│ Cache │─────────────▶│   DB   └────────┘         └───────┘              └────────┘
   (항상 일관성 보장, 쓰기 지연 증가)

3. Write-Back (Write-Behind):
   ┌────────┐  1.쓰기  ┌───────┐  2.비동기 배치 쓰기  ┌────────┐
App   │────────▶│ Cache │ ═════════════════▶│   DB   └────────┘         └───────┘                   └────────┘
   (쓰기 성능 우수, 데이터 유실 위험)

4. Write-Around:
   ┌────────┐  1.쓰기  ┌────────┐
App   │────────▶│   DB   └────┬───┘         └────────┘
2.읽기시 캐시 로드
   ┌───────┐
Cache   └───────┘

4.2 Redis vs Memcached

# Redis - 다양한 데이터 구조 지원
import redis

r = redis.Redis(host='localhost', port=6379)

# String
r.set('user:1001:name', 'Alice', ex=3600)  # 1시간 TTL

# Hash
r.hset('user:1001', mapping={
    'name': 'Alice',
    'email': 'alice@example.com',
    'login_count': 42
})

# Sorted Set (리더보드)
r.zadd('leaderboard', {'player_a': 1500, 'player_b': 1800})
top_10 = r.zrevrange('leaderboard', 0, 9, withscores=True)

# Pub/Sub
r.publish('notifications', 'New message!')
특성RedisMemcached
데이터 구조String, Hash, List, Set, Sorted Set 등Key-Value만
지속성RDB/AOF 지원없음
복제Master-Replica없음
클러스터Redis Cluster클라이언트 측 샤딩
메모리 효율상대적으로 낮음높음 (단순 구조)
사용 사례세션, 리더보드, 큐, Pub/Sub단순 캐싱

4.3 CDN (Content Delivery Network)

CDN이 없는 경우:
사용자(서울) ──── 5000km ────▶ Origin Server(미국)
RTT: ~200ms

CDN이 있는 경우:
사용자(서울) ──── 50km ────▶ CDN Edge(서울) ── cache hit
RTT: ~5ms

Cache Miss시:
사용자(서울)CDN Edge(서울)Origin(미국)
                ↓ 캐시 저장
         다음 요청부터 Edge에서 응답

CDN 캐시 무효화 전략:

1. TTL 기반: Cache-Control: max-age=86400
2. 버전 관리: /static/app.v2.3.1.js
3. 수동 무효화: CloudFront Invalidation
4. 태그 기반: Surrogate-Key 헤더로 선택적 무효화

4.4 캐시 문제와 해결

1. Cache Stampede (캐시 쓰나미):
   TTL 만료 → 수천 요청이 동시에 DB   해결: 뮤텍스 락 + 사전 갱신

2. Cache Penetration (캐시 관통):
   존재하지 않는 키 반복 조회 → 매번 DB 조회
   해결: Bloom Filter + 빈 결과도 캐싱

3. Hot Key Problem:
   특정 키에 트래픽 집중 → 단일 캐시 노드 과부하
   해결: 로컬 캐시 +복제 (key_1, key_2, ...)

5. 데이터베이스 설계

5.1 SQL vs NoSQL

기준SQL (RDBMS)NoSQL
데이터 모델정규화된 테이블Document, Key-Value, Column, Graph
스키마고정 스키마유연한 스키마
확장 방식수직 확장 위주수평 확장 용이
트랜잭션ACID 완벽 지원제한적 (일부 지원)
조인효율적 지원제한적/불가
적합한 경우관계 복잡, 트랜잭션 중요대용량, 유연한 스키마, 고성능
대표 제품PostgreSQL, MySQLMongoDB, Cassandra, DynamoDB
SQL 선택 기준:
- 데이터 관계가 복잡하고 조인이 빈번
- ACID 트랜잭션이 필수 (금융, 결제)
- 데이터 무결성이 최우선
- 스키마가 안정적

NoSQL 선택 기준:
- 데이터 구조가 자주 변경
- 초대량 데이터 (TB 이상)
- 매우 낮은 지연 시간 필요
- 수평 확장이 필수
- 비정형 데이터 (로그, 소셜 미디어)

5.2 데이터베이스 복제 (Replication)

Master-Slave 복제:
┌──────────┐     동기/비동기 복제     ┌──────────┐
Master  │ ═══════════════════▶│  Slave 1 │  ← 읽기 전용
 (Write)  │ ═══════════════════▶│  Slave 2 │  ← 읽기 전용
└──────────┘                     └──────────┘

Multi-Master 복제:
┌──────────┐ ◀═══════════════▶ ┌──────────┐
Master 1 │    양방향 복제     │ Master 2 (R/W) (R/W)└──────────┘                   └──────────┘
(충돌 해결 필요)

5.3 데이터베이스 샤딩 (Sharding)

샤딩 전:
┌─────────────────────┐
Single Database│   10TB, 지연 증가    │
└─────────────────────┘

샤딩  (Hash 기반):
hash(user_id) % 4 = shard_number

┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
Shard 0 │ │ Shard 1 │ │ Shard 2 │ │ Shard 32.5TB   │ │ 2.5TB   │ │ 2.5TB   │ │ 2.5TB   │
└─────────┘ └─────────┘ └─────────┘ └─────────┘

샤딩 전략 비교:

전략장점단점
Range-based범위 쿼리 효율적핫스팟 발생 가능
Hash-based균등 분배범위 쿼리 비효율
Directory-based유연한 매핑디렉토리가 SPOF
Geographic지역성 활용지역 간 쿼리 어려움

샤딩 시 고려사항:

1. 조인 쿼리: 샤드 간 조인 불가 → 비정규화 또는 애플리케이션 레벨 조인
2. 리밸런싱: 데이터 증가시 샤드 재분배 → Consistent Hashing 활용
3. 핫스팟: 특정 샤드에 트래픽 집중 → 샤드 키 재설계
4. 트랜잭션: 분산 트랜잭션 → Saga 패턴 또는 2PC
5. 고유 ID: 글로벌 유니크 ID 필요 → Snowflake ID, UUID

5.4 비정규화 (Denormalization)

-- 정규화된 설계 (조인 필요):
SELECT u.name, o.total, p.name AS product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;

-- 비정규화된 설계 (단일 테이블):
-- orders 테이블에 user_name, product_name 포함
SELECT user_name, total, product_name
FROM denormalized_orders
WHERE user_id = 1001;

비정규화는 읽기 성능을 향상시키지만, 데이터 중복과 쓰기 복잡성을 증가시킵니다. 읽기가 월등히 많은 경우에 적합합니다.


6. CAP 정리와 PACELC

6.1 CAP Theorem

       Consistency (일관성)
         /          \
        /            \
       /     CA      \
      /  (single DC)  \
     /________________\
    |                  |
 CP |    분산 시스템    | AP
    |  P는 피할 수 없음  |
    |__________________|
         Availability
         (가용성)

네트워크 파티션 발생시:
CP 시스템 → 일관성 유지, 일부 요청 거부
AP 시스템 → 가용성 유지, 일시적 불일치 허용

실제 시스템의 CAP 선택:

시스템선택이유
은행 계좌CP잔액 불일치 불가
DNSAP약간 오래된 레코드 허용
MongoDBCP강한 일관성 기본
CassandraAP결국 일관성 (Eventual)
ZooKeeperCP분산 코디네이션

6.2 PACELC 이론

CAP을 확장한 PACELC은 파티션이 없을 때의 트레이드오프도 설명합니다.

if (Partition) then
    Choose between Availability and Consistency
else
    Choose between Latency and Consistency

예시:
- DynamoDB: PA/EL (가용성 + 낮은 지연)
- MongoDB:  PC/EC (일관성 우선)
- Cassandra: PA/EL (가용성 + 낮은 지연)
- PostgreSQL(single): PC/EC (일관성 + 일관성)

7. 메시지 큐 (Message Queue)

7.1 비동기 처리의 필요성

동기 처리:
ClientAPI → 이메일 전송(3s) → 이미지 리사이즈(2s) → 로그 저장(1s)Response
6초 대기

비동기 처리:
ClientAPIMessage QueueResponse (즉시)
             ┌──────┴──────┐
             ▼             ▼
       Email Worker   Image Worker
         (3s)           (2s)
0.1초 대기

7.2 Kafka vs RabbitMQ vs SQS

Apache Kafka:
┌──────────┐     ┌─────────────────────────┐
Producer │────▶│  Topic: orders          │
└──────────┘     │  ┌─────┬─────┬─────┐   │
                 │  │ P0P1P2  │   │
                 │  └──┬──┴──┬──┴──┬──┘   │
                 └─────┼─────┼─────┼──────┘
                       ▼     ▼     ▼
                 ┌──────────────────────┐
Consumer Group A                   (각 파티션 1 소비자)                 └──────────────────────┘
특성KafkaRabbitMQSQS
모델로그 기반 스트리밍AMQP 메시지 브로커관리형 큐
처리량초당 수백만 메시지초당 수만 메시지자동 확장
메시지 보존설정 기간 보존소비 후 삭제14일
순서 보장파티션 내 보장큐 단위 보장FIFO 큐 사용시
소비 모델Pull (Consumer Poll)Push + PullPull (Long Polling)
사용 사례이벤트 스트리밍, 로그작업 큐, RPC서버리스, 디커플링

7.3 메시지 전달 보장

1. At-Most-Once (최대 1):
   ProducerBroker (ACK 안 기다림)
   빠르지만 메시지 유실 가능
   사용: 로그, 메트릭 (유실 허용)

2. At-Least-Once (최소 1):
   ProducerBrokerACK
   ACK 실패시 재전송 → 중복 가능
   사용: 대부분의 시스템 (멱등 처리 필요)

3. Exactly-Once (정확히 1):
   트랜잭션 기반 또는 멱등 키 사용
   성능 오버헤드 있음
   사용: 금융, 결제 (정확성 필수)

8. 레이트 리미팅 (Rate Limiting)

8.1 Token Bucket 알고리즘

import time
import threading

class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity = capacity        # 버킷 최대 토큰 수
        self.tokens = capacity           # 현재 토큰 수
        self.refill_rate = refill_rate   # 초당 추가 토큰
        self.last_refill = time.time()
        self.lock = threading.Lock()

    def allow_request(self):
        with self.lock:
            now = time.time()
            # 토큰 보충
            elapsed = now - self.last_refill
            self.tokens = min(
                self.capacity,
                self.tokens + elapsed * self.refill_rate
            )
            self.last_refill = now

            # 요청 허용 여부
            if self.tokens >= 1:
                self.tokens -= 1
                return True
            return False

# 초당 10개 요청, 버스트 최대 20개
limiter = TokenBucket(capacity=20, refill_rate=10)

8.2 Sliding Window Log 알고리즘

import time
from collections import deque

class SlidingWindowLog:
    def __init__(self, window_size, max_requests):
        self.window_size = window_size   # 윈도우 크기 (초)
        self.max_requests = max_requests
        self.requests = deque()

    def allow_request(self):
        now = time.time()
        # 윈도우 밖의 오래된 요청 제거
        while self.requests and self.requests[0] < now - self.window_size:
            self.requests.popleft()

        if len(self.requests) < self.max_requests:
            self.requests.append(now)
            return True
        return False

# 1분에 최대 100개 요청
limiter = SlidingWindowLog(window_size=60, max_requests=100)

8.3 Sliding Window Counter

Sliding Window Counter (메모리 효율적):

이전 윈도우 카운트: 84
현재 윈도우 카운트: 36
윈도우 크기: 60현재 위치: 윈도우의 25% 지점

가중 카운트 = 84 * (1 - 0.25) + 36 = 84 * 0.75 + 36 = 99

max_requests = 100허용 (99 < 100)
알고리즘메모리정확도구현 복잡도
Token BucketO(1)높음낮음
Leaky BucketO(1)높음낮음
Sliding Window LogO(N)매우 높음중간
Sliding Window CounterO(1)근사치중간
Fixed WindowO(1)낮음매우 낮음

9. 실전 설계 예제

9.1 URL Shortener 설계

요구사항:

  • 1억 DAU, 읽기:쓰기 = 100:1
  • 단축 URL은 가능한 짧게 (7자)
  • 커스텀 URL 지원
  • 리디렉션 지연 시간 100ms 이하

Back-of-Envelope 계산:

쓰기: 1DAU * 평균 0.1 URL/day = 1000/day
QPS(쓰기) = 10,000,000 / 86,400 = ~116 QPS
QPS(읽기) = 116 * 100 = ~11,600 QPS
피크 QPS = 11,600 * 3 = ~35,000 QPS

저장 용량 (5):
10,000,000 * 365 * 5 = 182.5억 레코드
각 레코드 ~500 bytes → 182.5* 500B = ~9.1TB

시스템 아키텍처:

┌────────┐     ┌──────┐     ┌─────────────┐
Client │────▶│  LB  │────▶│  API Server└────────┘     └──────┘     └──────┬──────┘
                    ┌──────────────┼──────────────┐
                    ▼              ▼              ▼
              ┌──────────┐  ┌──────────┐  ┌──────────┐
Redis   │  │ MySQL    │  │ Analytics│
Cache (Sharded)  (Kafka)              └──────────┘  └──────────┘  └──────────┘

URL 인코딩 전략:

import hashlib
import base64

def generate_short_url(long_url, counter):
    # 방법 1: Base62 인코딩 (auto-increment ID)
    chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    short = ""
    n = counter
    while n > 0:
        short = chars[n % 62] + short
        n //= 62
    return short  # 예: "dK4x9Z"

    # 방법 2: MD5 해시 + Base62 (처음 7자)
    hash_val = hashlib.md5(long_url.encode()).hexdigest()
    num = int(hash_val[:12], 16)
    short = ""
    for _ in range(7):
        short = chars[num % 62] + short
        num //= 62
    return short

# 62^7 = 약 3.5조 개의 고유 URL 가능

9.2 Twitter/X 피드 설계

핵심 문제: Fan-out

Fan-out on Write (Push 모델):
사용자가 트윗 작성시 모든 팔로워의 타임라인에 미리 저장

User A 트윗 → Fan-out Service
                ├─▶ Follower 1의 타임라인 캐시에 추가
                ├─▶ Follower 2의 타임라인 캐시에 추가
                └─▶ Follower N의 타임라인 캐시에 추가

장점: 읽기가 매우 빠름 (미리 계산됨)
단점: 팔로워가 많으면 쓰기 부하 엄청남

Fan-out on Read (Pull 모델):
타임라인 요청시 팔로잉 중인 사용자들의 트윗을 실시간 조합

User B 타임라인 요청 → 팔로잉 사용자 조회
                        ├─▶ User X의 최신 트윗
                        ├─▶ User Y의 최신 트윗
                        └─▶ User Z의 최신 트윗
                        → 병합 + 정렬 → 반환

장점: 쓰기가 단순
단점: 읽기 시 여러 DB 조회 필요

Celebrity Problem 해결 (하이브리드):

일반 사용자 (팔로워 < 10,000): Fan-out on Write
셀러브리티 (팔로워 > 10,000): Fan-out on Read

타임라인 조회:
1. 캐시에서 미리 팬아웃된 트윗 가져오기
2. 팔로잉 중인 셀러브리티의 최신 트윗 조회
3. 두 결과를 병합 + 시간순 정렬

9.3 채팅 시스템 설계

WebSocket 기반 실시간 통신:

┌────────┐  WebSocket  ┌─────────────┐
User A │◀══════════▶│  Chat└────────┘             │  ServerCluster┌────────┐  WebSocket  │            │
User B │◀══════════▶│            │
└────────┘             └─────┬──────┘
                    ┌────────┼────────┐
                    ▼        ▼        ▼
              ┌────────┐ ┌──────┐ ┌────────┐
Redis  │ │Kafka │ │Message │
              │Presence│ │      │ │   DB              └────────┘ └──────┘ └────────┘

메시지 순서 보장:

방법 1: 서버 타임스탬프 (단순하지만 시계 동기화 문제)
방법 2: Snowflake ID (시간 + 서버 + 시퀀스)

Snowflake ID 구조 (64비트):
┌─────────────┬────────┬──────────┬──────────────┐
Timestamp  │Datactr │ MachineSequence41 bits   │ 5 bits │  5 bits  │   12 bits    │
└─────────────┴────────┴──────────┴──────────────┘
→ 시간순 정렬 가능 + 전역 고유
→ 초당 4096 * 1024 = ~400ID 생성 가능

온라인 상태 (Presence) 관리:

Heartbeat 방식:
- 클라이언트가 5초마다 heartbeat 전송
- 30초 동안 heartbeat 없으면 offline 처리
- Redis에 마지막 heartbeat 시간 저장

상태 변경 알림:
- 변경시 해당 사용자의 친구 목록에 알림
- 친구가 많으면 Fan-out 부하 → 접속 중인 친구에게만 알림

9.4 Netflix/비디오 스트리밍 설계

비디오 업로드 파이프라인:

┌──────────┐    ┌──────────┐    ┌─────────────────┐
Uploader │───▶│  Object  │───▶│  Transcoding│          │    │  Store   │    │  Pipeline└──────────┘      (S3)    │    │                 │
                └──────────┘    │  ┌───────────┐  │
                                │  │ 240p      │  │
                                │  │ 480p      │  │
                                │  │ 720p      │  │
                                │  │ 1080p     │  │
                                │  │ 4K        │  │
                                │  └───────────┘  │
                                └────────┬────────┘
                                ┌─────────────────┐
CDN에 배포    │
                                 (전 세계 Edge)                                └─────────────────┘

Adaptive Bitrate Streaming (ABR):

사용자 네트워크 상태에 따라 화질 자동 조정:

대역폭 10Mbps → 1080p 세그먼트 요청
대역폭 3Mbps  → 480p 세그먼트로 전환
대역폭 회복   → 점진적으로 화질 올림

HLS (HTTP Live Streaming) 방식:
1. 비디오를 2~10초 세그먼트로 분할
2. 각 세그먼트를 여러 화질로 인코딩
3. m3u8 매니페스트 파일로 세그먼트 목록 제공
4. 클라이언트가 대역폭에 맞는 세그먼트 선택

10. Back-of-Envelope 추정

10.1 자주 사용하는 숫자

지연 시간:
- L1 캐시 참조: ~1ns
- L2 캐시 참조: ~4ns
- 메인 메모리 참조: ~100ns
- SSD 랜덤 읽기: ~150us
- HDD Seek: ~10ms
- 동일 DC 왕복: ~0.5ms
- 미국 ↔ 유럽 왕복: ~75ms

용량:
- 1 char = 1 byte (ASCII) / 2~4 bytes (UTF-8)
- 1 UUID = 36 bytes (문자열) / 16 bytes (바이너리)
- 1 이미지 (중간) = ~200KB
- 1 비디오 (1, 720p) = ~50MB

일간 환산:
- 1= 86,400= ~100,000 (간단 계산)
- 1백만 요청/= ~12 QPS
- 1억 요청/= ~1,200 QPS

10.2 QPS 계산 예시

Twitter 규모 추정:
- DAU: 3억 명
- 평균 트윗 조회: 100/- 읽기 QPS = 3* 100 / 86,400 = ~347,000 QPS
- 피크 QPS = 347,000 * 3 = ~1,000,000 QPS

- 평균 트윗 작성: 2/- 쓰기 QPS = 3* 2 / 86,400 = ~6,944 QPS

저장소:
- 트윗 크기: ~300 bytes (텍스트 + 메타데이터)
- 하루: 6억 트윗 * 300B = ~180GB/- 5: 180GB * 365 * 5 = ~328TB
- 미디어 포함: ~5= ~1.6PB

11. 흔한 실수와 팁

11.1 피해야 할 실수

1. 요구사항 확인 없이 바로 설계 시작
   → 반드시 5분은 질문에 투자

2. 하나의 컴포넌트에만 집중
   → 전체 그림을 먼저, 면접관이 요청한 부분을 깊이

3. 완벽한 설계를 추구
   → 트레이드오프를 설명하는 것이 더 중요

4. 숫자 없는 설계
QPS, 저장량, 대역폭을 반드시 계산

5. 일방적으로 말하기
   → 면접관과 대화하며 피드백 수용

6. 과도한 기술 도입
   → 문제에 맞는 적절한 기술 선택

11.2 면접 팁

1. 구조화된 접근:
   - 4단계 프레임워크를 항상 따르기
   - 각 단계에서 시간 관리

2. 트레이드오프 설명:
   - "이 방법의 장점은 A이고, 단점은 B입니다."
   - "대안으로 C가 있지만, 우리 상황에서는 D가 더 적합합니다."

3. 실제 사례 언급:
   - "Netflix는 이 문제를 X 방식으로 해결했습니다."
   - "이전 프로젝트에서 비슷한 문제를 경험했습니다."

4. 화이트보드 활용:
   - 깔끔한 다이어그램
   - 데이터 흐름 화살표
   - 숫자/메트릭 표시

5. 면접관 힌트 수용:
   - 면접관의 질문은 곧 힌트
   - "좋은 지적입니다, 그 부분을 개선하면..."

12. 퀴즈

Q1. 확장성

수평 확장과 수직 확장의 주요 차이점 3가지를 설명하세요.

답변:

  1. 비용 구조: 수직 확장은 고사양 장비로 비용이 급증하지만, 수평 확장은 저사양 장비를 추가하므로 선형적 비용 증가
  2. 장애 격리: 수직 확장은 단일 장애 지점(SPOF)이지만, 수평 확장은 한 서버 장애가 전체에 영향 없음
  3. 확장 한계: 수직 확장은 물리적 한계가 있지만, 수평 확장은 이론적으로 무제한

수평 확장의 단점은 데이터 일관성 관리, 세션 관리, 운영 복잡도가 증가한다는 것입니다.

Q2. CAP 정리

은행 시스템은 왜 CP를 선택하나요? AP를 선택하면 어떤 문제가 발생하나요?

답변: 은행 시스템이 CP를 선택하는 이유는 잔액 일관성이 절대적으로 중요하기 때문입니다. AP를 선택하면 네트워크 파티션 발생 시 두 노드에서 동시에 출금이 가능해져, 실제 잔액보다 더 많은 금액이 출금되는 이중 지출(Double Spending) 문제가 발생할 수 있습니다. 이는 금융 손실로 직결되므로, 가용성을 일부 포기하더라도 일관성을 보장해야 합니다.

Q3. 캐싱

Cache Stampede(캐시 쓰나미)란 무엇이며, 어떻게 방지하나요?

답변: Cache Stampede는 인기 있는 캐시 항목의 TTL이 만료될 때, 수많은 요청이 동시에 DB에 쏟아지는 현상입니다.

방지 방법:

  1. 뮤텍스/분산 락: 첫 번째 요청만 DB에서 가져오고, 나머지는 대기
  2. 사전 갱신: TTL 만료 전에 백그라운드에서 갱신 (TTL의 80% 시점)
  3. TTL 랜덤화: 동일 시간에 만료되지 않도록 약간의 랜덤값 추가
  4. 외부 갱신: 전용 워커가 주기적으로 캐시 갱신

Q4. 메시지 큐

Kafka와 RabbitMQ를 각각 어떤 상황에서 선택해야 하나요?

답변: Kafka 선택:

  • 이벤트 스트리밍 (로그 수집, 실시간 분석)
  • 높은 처리량 필요 (초당 수백만 메시지)
  • 메시지 재처리 필요 (소비 후에도 보존)
  • 이벤트 소싱 패턴

RabbitMQ 선택:

  • 전통적인 작업 큐 (이메일 전송, 이미지 처리)
  • 복잡한 라우팅 필요 (Exchange, Routing Key)
  • 메시지 우선순위 필요
  • RPC 패턴
  • 메시지 단위 ACK 필요

핵심 차이: Kafka는 "이벤트 로그"이고, RabbitMQ는 "메시지 브로커"입니다.

Q5. 실전 설계

URL Shortener에서 Base62 인코딩을 사용하는 이유와 해시 충돌을 어떻게 처리하나요?

답변: Base62 사용 이유:

  • URL-safe 문자만 사용 (0-9, a-z, A-Z)
  • 62^7 = 약 3.5조 개의 고유 URL 생성 가능
  • 짧은 문자열로 큰 숫자 표현 가능

해시 충돌 처리:

  1. Auto-increment ID + Base62: 충돌 없음 (추천)
  2. MD5/SHA256 + 앞 7자: 충돌 시 뒤에 문자 추가
  3. 충돌 감지 + 재시도: DB에서 중복 검사 후 다른 해시 시도
  4. Bloom Filter: 존재 여부 빠르게 확인, 충돌 시 재생성

실무에서는 방법 1 (분산 환경에서 Snowflake ID 사용)이 가장 신뢰성 있습니다.


참고 자료

  1. Alex Xu, "System Design Interview - An Insider's Guide" (2020)
  2. Alex Xu, "System Design Interview Volume 2" (2022)
  3. Martin Kleppmann, "Designing Data-Intensive Applications" (2017)
  4. Google SRE Book - https://sre.google/sre-book/table-of-contents/
  5. AWS Well-Architected Framework - https://aws.amazon.com/architecture/well-architected/
  6. Meta Engineering Blog - https://engineering.fb.com/
  7. Netflix Tech Blog - https://netflixtechblog.com/
  8. Uber Engineering Blog - https://eng.uber.com/
  9. Cassandra vs MongoDB vs DynamoDB - https://db-engines.com/en/system/Amazon+DynamoDB%3BCassandra%3BMongoDB
  10. Kafka Documentation - https://kafka.apache.org/documentation/
  11. Redis Documentation - https://redis.io/documentation
  12. Consistent Hashing Paper - Karger et al. (1997)
  13. Dynamo Paper - Amazon (2007)
  14. MapReduce Paper - Google (2004)