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

- Name
- Youngju Kim
- @fjvbn20031
- 서론
- 1. 시스템 디자인 면접 4단계 프레임워크
- 2. 확장성 (Scalability)
- 3. 로드 밸런싱 (Load Balancing)
- 4. 캐싱 (Caching)
- 5. 데이터베이스 설계
- 6. CAP 정리와 PACELC
- 7. 메시지 큐 (Message Queue)
- 8. 레이트 리미팅 (Rate Limiting)
- 9. 실전 설계 예제
- 10. Back-of-Envelope 추정
- 11. 흔한 실수와 팁
- 12. 퀴즈
- 참고 자료
서론
시스템 디자인 면접은 시니어 엔지니어 채용에서 가장 중요한 평가 항목입니다. 알고리즘 문제가 코딩 능력을 측정한다면, 시스템 디자인은 대규모 시스템을 설계하고 트레이드오프를 이해하는 종합적인 엔지니어링 역량을 평가합니다.
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 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 - 다양한 데이터 구조 지원
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!')
| 특성 | 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 알고리즘
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 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 인코딩 전략:
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 │
└────────┘ │ 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. 확장성
수평 확장과 수직 확장의 주요 차이점 3가지를 설명하세요.
답변:
- 비용 구조: 수직 확장은 고사양 장비로 비용이 급증하지만, 수평 확장은 저사양 장비를 추가하므로 선형적 비용 증가
- 장애 격리: 수직 확장은 단일 장애 지점(SPOF)이지만, 수평 확장은 한 서버 장애가 전체에 영향 없음
- 확장 한계: 수직 확장은 물리적 한계가 있지만, 수평 확장은 이론적으로 무제한
수평 확장의 단점은 데이터 일관성 관리, 세션 관리, 운영 복잡도가 증가한다는 것입니다.
Q2. CAP 정리
은행 시스템은 왜 CP를 선택하나요? AP를 선택하면 어떤 문제가 발생하나요?
답변: 은행 시스템이 CP를 선택하는 이유는 잔액 일관성이 절대적으로 중요하기 때문입니다. AP를 선택하면 네트워크 파티션 발생 시 두 노드에서 동시에 출금이 가능해져, 실제 잔액보다 더 많은 금액이 출금되는 이중 지출(Double Spending) 문제가 발생할 수 있습니다. 이는 금융 손실로 직결되므로, 가용성을 일부 포기하더라도 일관성을 보장해야 합니다.
Q3. 캐싱
Cache Stampede(캐시 쓰나미)란 무엇이며, 어떻게 방지하나요?
답변: Cache Stampede는 인기 있는 캐시 항목의 TTL이 만료될 때, 수많은 요청이 동시에 DB에 쏟아지는 현상입니다.
방지 방법:
- 뮤텍스/분산 락: 첫 번째 요청만 DB에서 가져오고, 나머지는 대기
- 사전 갱신: TTL 만료 전에 백그라운드에서 갱신 (TTL의 80% 시점)
- TTL 랜덤화: 동일 시간에 만료되지 않도록 약간의 랜덤값 추가
- 외부 갱신: 전용 워커가 주기적으로 캐시 갱신
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 생성 가능
- 짧은 문자열로 큰 숫자 표현 가능
해시 충돌 처리:
- Auto-increment ID + Base62: 충돌 없음 (추천)
- MD5/SHA256 + 앞 7자: 충돌 시 뒤에 문자 추가
- 충돌 감지 + 재시도: DB에서 중복 검사 후 다른 해시 시도
- Bloom Filter: 존재 여부 빠르게 확인, 충돌 시 재생성
실무에서는 방법 1 (분산 환경에서 Snowflake ID 사용)이 가장 신뢰성 있습니다.
참고 자료
- Alex Xu, "System Design Interview - An Insider's Guide" (2020)
- Alex Xu, "System Design Interview Volume 2" (2022)
- Martin Kleppmann, "Designing Data-Intensive Applications" (2017)
- Google SRE Book - https://sre.google/sre-book/table-of-contents/
- AWS Well-Architected Framework - https://aws.amazon.com/architecture/well-architected/
- Meta Engineering Blog - https://engineering.fb.com/
- Netflix Tech Blog - https://netflixtechblog.com/
- Uber Engineering Blog - https://eng.uber.com/
- Cassandra vs MongoDB vs DynamoDB - https://db-engines.com/en/system/Amazon+DynamoDB%3BCassandra%3BMongoDB
- Kafka Documentation - https://kafka.apache.org/documentation/
- Redis Documentation - https://redis.io/documentation
- Consistent Hashing Paper - Karger et al. (1997)
- Dynamo Paper - Amazon (2007)
- MapReduce Paper - Google (2004)