Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

서론

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

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

이 가이드에서는 시스템 디자인 면접의 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 2 │

│ Balancer │ │ └─────────────┘

└──────────┘ │ ┌─────────────┐

├────▶│ 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 | 시간 기반 | 예측 가능한 패턴 |

| Predictive | ML 기반 | 반복적 패턴 |

3. 로드 밸런싱 (Load Balancing)

3.1 로드 밸런싱 알고리즘

1. Round Robin (순차 배분)

Request 1 → Server A

Request 2 → Server B

Request 3 → Server C

Request 4 → Server 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 종료, 더 유연함

| 특성 | L4 | L7 |

|------|-----|-----|

| 계층 | TCP/UDP | HTTP/HTTPS |

| 성능 | 매우 빠름 | 상대적으로 느림 |

| 라우팅 | IP/Port | URL, Header, Cookie |

| SSL 종료 | 불가 | 가능 |

| 사용 예 | AWS NLB | AWS 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 - 다양한 데이터 구조 지원

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!')

| 특성 | Redis | Memcached |

|------|-------|-----------|

| 데이터 구조 | 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, MySQL | MongoDB, 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 3 │

│ 2.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 | 잔액 불일치 불가 |

| DNS | AP | 약간 오래된 레코드 허용 |

| MongoDB | CP | 강한 일관성 기본 |

| Cassandra | AP | 결국 일관성 (Eventual) |

| ZooKeeper | CP | 분산 코디네이션 |

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 비동기 처리의 필요성

동기 처리:

Client → API → 이메일 전송(3s) → 이미지 리사이즈(2s) → 로그 저장(1s) → Response

총 6초 대기

비동기 처리:

Client → API → Message Queue → Response (즉시)

┌──────┴──────┐

▼ ▼

Email Worker Image Worker

(3s) (2s)

총 0.1초 대기

7.2 Kafka vs RabbitMQ vs SQS

Apache Kafka:

┌──────────┐ ┌─────────────────────────┐

│ Producer │────▶│ Topic: orders │

└──────────┘ │ ┌─────┬─────┬─────┐ │

│ │ P0 │ P1 │ P2 │ │

│ └──┬──┴──┬──┴──┬──┘ │

└─────┼─────┼─────┼──────┘

▼ ▼ ▼

┌──────────────────────┐

│ Consumer Group A │

│ (각 파티션 1 소비자) │

└──────────────────────┘

| 특성 | Kafka | RabbitMQ | SQS |

|------|-------|----------|-----|

| 모델 | 로그 기반 스트리밍 | AMQP 메시지 브로커 | 관리형 큐 |

| 처리량 | 초당 수백만 메시지 | 초당 수만 메시지 | 자동 확장 |

| 메시지 보존 | 설정 기간 보존 | 소비 후 삭제 | 14일 |

| 순서 보장 | 파티션 내 보장 | 큐 단위 보장 | FIFO 큐 사용시 |

| 소비 모델 | Pull (Consumer Poll) | Push + Pull | Pull (Long Polling) |

| 사용 사례 | 이벤트 스트리밍, 로그 | 작업 큐, RPC | 서버리스, 디커플링 |

7.3 메시지 전달 보장

1. At-Most-Once (최대 1회):

Producer → Broker (ACK 안 기다림)

빠르지만 메시지 유실 가능

사용: 로그, 메트릭 (유실 허용)

2. At-Least-Once (최소 1회):

Producer → Broker → ACK

ACK 실패시 재전송 → 중복 가능

사용: 대부분의 시스템 (멱등 처리 필요)

3. Exactly-Once (정확히 1회):

트랜잭션 기반 또는 멱등 키 사용

성능 오버헤드 있음

사용: 금융, 결제 (정확성 필수)

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

8.1 Token Bucket 알고리즘

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 알고리즘

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 Bucket | O(1) | 높음 | 낮음 |

| Leaky Bucket | O(1) | 높음 | 낮음 |

| Sliding Window Log | O(N) | 매우 높음 | 중간 |

| Sliding Window Counter | O(1) | 근사치 | 중간 |

| Fixed Window | O(1) | 낮음 | 매우 낮음 |

9. 실전 설계 예제

9.1 URL Shortener 설계

**요구사항:**

- 1억 DAU, 읽기:쓰기 = 100:1

- 단축 URL은 가능한 짧게 (7자)

- 커스텀 URL 지원

- 리디렉션 지연 시간 100ms 이하

**Back-of-Envelope 계산:**

쓰기: 1억 DAU * 평균 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 인코딩 전략:**

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 │

└────────┘ │ Server │

│ Cluster │

┌────────┐ WebSocket │ │

│ User B │◀══════════▶│ │

└────────┘ └─────┬──────┘

┌────────┼────────┐

▼ ▼ ▼

┌────────┐ ┌──────┐ ┌────────┐

│ Redis │ │Kafka │ │Message │

│Presence│ │ │ │ DB │

└────────┘ └──────┘ └────────┘

**메시지 순서 보장:**

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

방법 2: Snowflake ID (시간 + 서버 + 시퀀스)

Snowflake ID 구조 (64비트):

┌─────────────┬────────┬──────────┬──────────────┐

│ Timestamp │Datactr │ Machine │ Sequence │

│ 41 bits │ 5 bits │ 5 bits │ 12 bits │

└─────────────┴────────┴──────────┴──────────────┘

→ 시간순 정렬 가능 + 전역 고유

→ 초당 4096 * 1024 = ~400만 ID 생성 가능

**온라인 상태 (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. 확장성

**답변:**

1. **비용 구조:** 수직 확장은 고사양 장비로 비용이 급증하지만, 수평 확장은 저사양 장비를 추가하므로 선형적 비용 증가

2. **장애 격리:** 수직 확장은 단일 장애 지점(SPOF)이지만, 수평 확장은 한 서버 장애가 전체에 영향 없음

3. **확장 한계:** 수직 확장은 물리적 한계가 있지만, 수평 확장은 이론적으로 무제한

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

Q2. CAP 정리

**답변:**

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

Q3. 캐싱

**답변:**

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

**방지 방법:**

1. **뮤텍스/분산 락:** 첫 번째 요청만 DB에서 가져오고, 나머지는 대기

2. **사전 갱신:** TTL 만료 전에 백그라운드에서 갱신 (TTL의 80% 시점)

3. **TTL 랜덤화:** 동일 시간에 만료되지 않도록 약간의 랜덤값 추가

4. **외부 갱신:** 전용 워커가 주기적으로 캐시 갱신

Q4. 메시지 큐

**답변:**

**Kafka 선택:**

- 이벤트 스트리밍 (로그 수집, 실시간 분석)

- 높은 처리량 필요 (초당 수백만 메시지)

- 메시지 재처리 필요 (소비 후에도 보존)

- 이벤트 소싱 패턴

**RabbitMQ 선택:**

- 전통적인 작업 큐 (이메일 전송, 이미지 처리)

- 복잡한 라우팅 필요 (Exchange, Routing Key)

- 메시지 우선순위 필요

- RPC 패턴

- 메시지 단위 ACK 필요

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

Q5. 실전 설계

**답변:**

**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)

현재 단락 (1/667)

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

작성 글자: 0원문 글자: 16,435작성 단락: 0/667