실제 규모에 맞는 시스템 설계 방법 — 실무적인 시스템 디자인 튜토리얼
요약
오버엔지니어링을 방지하고 실제 트래픽 규모에 맞는 실무적인 시스템 설계 방법을 다룹니다. 사용자 수와 요청량에 따른 적절한 기술 스택 선택과 데이터베이스 활용 전략을 제시합니다.
핵심 포인트
- 실제 부하(Load)를 계산하여 적절한 인프라 규모 결정
- 초기 단계에서는 Kubernetes나 샤딩 같은 복잡한 기술 지양
- 데이터 일관성이 중요하다면 PostgreSQL 등 SQL 우선 사용
- 캐싱 도입 전 데이터베이스 인덱스 최적화 선행
실제 규모에 맞는 시스템 설계 방법 — 실무적인 시스템 디자인 튜토리얼
일상적인 애플리케이션을 위한 시스템 디자인: 실제 트래픽에 맞춘 설계
대부분의 시스템 디자인 콘텐츠는 첫날부터 수백만 명의 사용자를 대상으로 구축한다고 가정합니다. 하지만 500명의 고객을 위한 SaaS (Software as a Service), 팀 내부용 도구, 또는 스타트업의 MVP (Minimum Viable Product)를 구축하고 있다면, FAANG 규모의 패턴은 시간과 비용을 낭비하는 오버엔지니어링 (Overengineering)입니다. 이 튜토리얼은 합리적인 용량 결정을 바탕으로 실제 세계의 트래픽에 대응하는 실무적인 시스템 디자인을 다룹니다.
실제 수치부터 시작하기
어떤 기술을 선택하기 전에, 실제 부하 (Load)를 계산하십시오:
| 지표 | 소규모 앱 (사용자 1K) | 중간 규모 앱 (사용자 50K) | 걱정해야 할 시점 |
|---|---|---|---|
| 일일 요청 수 | ~10K-50K | ~500K-2M | >5M/day |
| ... |
사용자당 하루 5회의 페이지 뷰가 발생하고 페이지당 크기가 50KB인 월간 활성 사용자(MAU) 10K를 기준으로 할 때:
- 일일 대역폭 (Daily bandwidth): 10K × 5 × 50KB = 2.5GB/day
- 피크 RPS (Peak Requests Per Second): ~1-2 requests/second
이 정도 규모에서는 Kubernetes (쿠버네티스), 샤딩 (Sharding), 또는 분산 캐시 (Distributed caches)가 필요하지 않습니다.
데이터베이스 선택: 먼저 단순한 것을 고르세요
SQL이 기본값입니다
다음과 같은 경우 PostgreSQL 또는 MySQL을 사용하세요:
- 트랜잭션 (Transactions)이 필요한 경우 (결제, 재고, 주문)
- 데이터 간의 관계가 명확한 경우 (사용자 → 주문 → 품목)
- 조인 (Joins)을 포함한 복잡한 쿼리가 필요한 경우
- 쓰기 속도보다 데이터 일관성 (Data consistency)이 더 중요한 경우
실제 사례: 50명의 고객을 보유한 이커머스 스토어는 PostgreSQL을 사용합니다. 월 20달러의 DigitalOcean 드롭릿 (Droplet)에 설치된 단일 인스턴스가 100건의 주문/일을 100ms 미만의 쿼리로 처리합니다. 여기에 읽기 복제본 (Read replicas)이나 샤딩을 추가하는 것은 돈 낭비가 될 것입니다.
NoSQL을 고려해야 할 때
문서 지향 데이터베이스 (Document databases, MongoDB)를 사용하는 경우:
- 스키마 (Schema)가 빈번하게 변경되는 경우 (다양한 속성을 가진 제품 카탈로그)
- 중첩된 JSON 데이터를 저장하는 경우 (사용자 설정, 로그)
- 마이그레이션 (Migrations) 없이 빠른 프로토타이핑 (Prototyping)이 필요한 경우
인메모리 데이터베이스 (In-memory databases, Redis)를 사용하는 경우:
- 10ms 미만의 지연 시간 (Latency)이 필요한 경우 (세션 저장소, 실시간 리더보드)
- 기본 저장소가 아닌 캐시 계층 (Cache layer)으로 사용하는 경우
다음과 같은 경우에는 NoSQL을 피하세요:
- 주로 단순한 키 기반 조회 (Key-based lookups)가 필요한 경우 (대신 캐시를 사용하세요)
- 금융 데이터와 같이 강력한 일관성 (Strong consistency)이 필요한 경우
캐싱 전 인덱스 설정 규칙 (The Index Before You Cache Rule)
Redis를 추가하기 전에 데이터베이스 쿼리를 최적화하세요:
-- 느린 쿼리 (전체 테이블 스캔)
SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';
...
적절한 인덱스 (Index)를 설정하는 것만으로도 인프라 복잡성을 추가하지 않고도 종종 10~100배의 속도 향상을 얻을 수 있습니다. 다음과 같은 경우에만 캐싱을 추가하세요:
- 인덱싱 후에도 쿼리가 여전히 느린 경우
- 동일한 데이터를 초당 수백 번 읽는 경우
- 약간의 데이터 신선도 저하 (수 초에서 수 분 정도)가 허용되는 경우
실제로 도움이 되는 캐싱 전략 (Caching Strategies That Actually Help)
언제 캐싱할 것인가 (When to Cache)
데이터가 자주 읽히지만 수정은 드물게 발생하는 경우에 캐싱하세요:
- 사용자 프로필 (하루에 100번 읽히고 1번 쓰임)
- 제품 카탈로그 (시간당 1000번 읽히고 1번 쓰임)
- 대시보드 집계 데이터 (5분마다 재계산되며 50번 읽힘)
캐싱하지 마세요:
- 실시간 재고 수량 (데이터가 오래되면 초과 판매가 발생할 수 있음)
- 매 요청마다 변경되는 사용자별 데이터
- 100행 미만의 데이터 (데이터베이스만으로도 충분히 빠름)
소규모 앱을 위한 캐시 패턴 (Cache Patterns for Small Apps)
1. 캐시 사이드 (Cache-Aside) (가장 단순하고 일반적임)
def get_user(user_id):
cache_key = f"user:{user_id}"
user = cache.get(cache_key)
...
- 캐시 미스 (Cache miss) 발생 시 데이터베이스에 접근합니다.
- 캐시 항목은 TTL(Time To Live) 이후 만료됩니다.
2. 정적 자산을 위한 CDN (CDN for Static Assets)
- 이미지, CSS, JavaScript → Cloudflare (무료 티어)
- 서버 부하를 60~80% 감소시킵니다.
- 코드 변경이 필요 없으며, 자산 URL만 업데이트하면 됩니다.
TTL 가이드라인 (TTL Guidelines)
| 데이터 유형 | 권장 TTL |
|---|---|
| 사용자 세션 | 30-60분 |
| ... |
TTL이 짧을수록 데이터는 더 신선하지만 캐시 미스가 더 많이 발생합니다. TTL이 길수록 속도는 빠르지만 데이터가 오래될 수 있습니다.
큐(Queue)를 사용할 때 vs. 최적화만 할 때 (When to Use a Queue vs. Just Optimize)
큐를 사용하는 경우 (Use a Queue When)
작업이 사용자의 즉각적인 응답에 포함되지 않는 경우:
- 회원가입 후 이메일 발송
- 이미지 업로드 처리 (크기 조정, 썸네일 생성)
- PDF 보고서 생성
- 제3자에게 웹훅 (Webhook) 전달
너무 많은 동시 요청으로 인해 타임아웃 (Timeout) 에러가 발생하는 경우:
- 큐 (Queue)를 통해 트래픽 급증을 처리하고 다운스트림 서비스 (Downstream services)로 전달되는 트래픽을 평활화 (Smooth) 합니다.
실제 사례: 5,000명의 사용자를 보유한 사진 앱이 이미지 처리를 위해 Redis Queue를 사용합니다. 업로드는 즉시 반환되며, 처리는 백그라운드 (Background)에서 진행됩니다. 큐가 없다면 피크 시간대에 30초가 소요되는 업로드 작업이 타임아웃됩니다.
큐 (Queue)를 사용하지 말아야 할 때
- 작업이 1초 미만으로 완료되는 경우 (그냥 동기적 (Synchronously)으로 처리하세요)
- 사용자에게 즉시 결과를 보여줘야 하는 경우
- 단순히 "비동기 (Async)"로 만들기 위해 큐를 추가하는 경우 (조기 최적화 (Premature optimization))
큐는 모니터링, 재시도 로직 (Retry logic), 데드 레터 큐 (Dead-letter queues), 메시지 순서 보장 (Message ordering) 등 운영 복잡성을 증가시킵니다. 동기식 코드가 잘 작동한다면, 그대로 동기식으로 유지하세요.
큐 (Queue) vs 데이터베이스 (Database): 주요 차이점
| 큐 (Queue) 최적화 대상 |
|---|
| 처리량 (Throughput) |
| 데이터베이스 (Database) 최적화 대상 |
|---|
| 내구성 (Durability) |
경험 법칙 (Rule of thumb): 데이터를 잃어버렸을 때 타격이 크다면, 큐에 그 데이터를 맡기지 마세요. 상태 (State)는 데이터베이스에 영구 저장하고, 큐는 이벤트를 전달하는 용도로 사용하세요.
용량 계획 (Capacity Planning): 합리적인 결정
작게 시작하고, 측정된 결과에 따라 확장하세요
대부분의 앱을 위한 1일 차 아키텍처 (Architecture):
- 단일 애플리케이션 서버 (2-4 CPU, 4-8GB RAM)
- 관리형 데이터베이스 (AWS RDS의 PostgreSQL, DigitalOcean, Supabase 등)
- 캐싱 (Caching)을 위한 Redis (선택 사항, 월 $5-10)
- CDN + DDoS 방어를 위한 Cloudflare 무료 티어
비용: 월간 사용자 1만 명 기준 약 $30-60/월
수평 확장 전 수직 확장을 먼저 하세요
수직 확장 (Vertical scaling, 하드웨어 업그레이드):
- 가장 빠른 경로: 4GB RAM에서 16GB RAM으로 업그레이드
- 코드 변경이 필요 없음
- 대부분의 앱에서 사용자 약 10만 명까지 유효함
수평 확장 (Horizontal scaling, 서버 추가):
- 상태가 없는 (Stateless) 애플리케이션 설계가 필요함
- 로드 밸런서 (Load balancer)의 복잡성이 추가됨
- 수직 확장이 한계에 도달했을 때만 필요함
확장을 위한 모니터링 트리거 (Monitoring Triggers)
다음 상황이 지속적으로 발생하면 용량을 추가하세요:
- 피크 시간 동안 CPU 사용률이 5분 이상 70% 초과
- 데이터베이스 커넥션 풀 (Connection pool)이 80% 이상 가득 참
- 요청의 10% 이상에서 응답 시간 (Response time)이 500ms 초과
- 에러율 (Error rate)이 1% 초과
“만약에”를 기반으로 확장하지 마세요. 실제 지표 (Metrics)를 기반으로 확장하세요.
오버엔지니어링 (Overengineering) 피하기: 실제 사례
사례 1: 블로그 플랫폼 (월간 독자 1만 명)
오버엔지니어링된 설계:
- Kubernetes 클러스터
- 3개의 노드를 가진 Redis 클러스터
- 읽기 복제본 (Read replicas)이 있는 PostgreSQL
- "비동기 댓글"을 위한 RabbitMQ
- 비용: 월 $400, 설정 기간 2주
실제로 필요한 설계:
- 월 $20 규모의 단일 VPS (DigitalOcean/Linode)
- SQLite 또는 단일 PostgreSQL 인스턴스
- 캐시 없음 (데이터베이스가 초당 100회의 읽기 요청을 쉽게 처리함)
- 동기식 댓글
- 비용: 월 $20, 설정 기간 1일
사례 2: 내부 대시보드 (사용자 50명)
오버엔지니어링된 설계:
- 마이크로서비스 아키텍처 (Microservices architecture)
- GraphQL API 계층
- 다중 데이터베이스 (PostgreSQL + MongoDB)
- 비용: 유지보수가 복잡하고 개발 속도가 느림
실제로 필요한 설계:
- 단일 Flask/Django/Express 앱
- 적절한 인덱스 (Indexes)가 설정된 PostgreSQL
- Flask-Caching 또는 유사한 도구를 사용한 서버 측 캐싱 (Server-side caching)
- 결과: 개발 속도 10배 향상, 디버깅 용이성 증대
사례 3: 고객 500명의 SaaS
큐 (Queue)를 추가해야 할 때:
- 이메일 알림이 동기식으로 처리될 때 2~3초가 소요됨
- 피크 시간대에 페이지 로딩 속도가 느리다는 사용자 불만이 발생함
- 이메일 전송만을 위해 Redis Queue 또는 BullMQ를 추가함
큐 (Queue)를 추가하지 말아야 할 때:
- 페이지 로딩이 200ms 이내임 (충분히 빠름)
- 백그라운드 작업 (Background jobs)이 500ms 미만으로 완료됨
- 팀 내에 큐 모니터링 (Queue monitoring) 경험이 없음
실무적인 의사결정 체크리스트
복잡성을 추가하기 전에 다음을 질문하세요:
- 실제 트래픽은 어느 정도인가? 계획을 세우기 전에 측정하세요.
- 쿼리 (Query)를 먼저 최적화할 수 있는가? 캐싱을 도입하기 전에 인덱스를 추가하세요.
- 이 작업이 사용자를 차단(Blocking)하는가? 아니라면 큐에 넣으세요. 그렇다면 동기식 (Sync)으로 유지하세요.
- 실패 시 비용은 얼마인가? 중요한 데이터라면 큐가 아닌 데이터베이스에 저장하세요.
- 먼저 수직 확장 (Scale vertically)을 할 수 있는가? 서버를 추가하기 전에 RAM/CPU를 업그레이드하세요.
- 우리 팀이 이것을 유지보수할 방법을 알고 있는가? 복잡성 = 유지보수 비용입니다.
효과적인 "지루한 기술 (Boring Technology)" 스택
일상적인 애플리케이션의 90%(사용자 10만 명까지)를 위한 구성:
| 계층 (Layer) | 기술 (Technology) | 이유 (Why) |
|---|---|---|
| 백엔드 (Backend) | Python (Django/Flask), Node.js, Ruby on Rails | 빠른 개발 속도, 거대한 생태계 |
| ... | ||
| 이 스택은 1~2명의 엔지니어가 유지보수하면서 월간 사용자 5만 명 이상을 처리할 수 있습니다. |
최종 규칙: WWDC가 아닌 다음 마일스톤을 위해 설계하라
현재 트래픽의 1000배가 아니라, 10배를 목표로 설계하세요. 사용자 수가 1,000명이라면 10,000명을 위해 설계하십시오. 10,000명에 도달했을 때, 엔지니어를 채용하고 리팩터링 (Refactor)을 할 수 있는 수익이 생길 것입니다. 대부분의 스타트업은 확장이 너무 늦어서가 아니라, 너무 느리게 움직여서 망합니다.
최고의 시스템 설계는 오늘날의 실제 사용자들에게 작동하는 가장 단순한 설계이며, 확장이 필요하다고 판단되는 시점에 명확한 확장 경로를 갖추고 있는 설계입니다.
Rizwan Saleem — https://rizwansaleem.co
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기