LabHub

블로그

스케일링 변곡점 완전 가이드: 100명에서 1000만 명까지, 아키텍처는 언제 어떻게 진화하는가 (2025)

들어가며 — "지금 Kubernetes가 필요한가?"

스타트업 창업자가 묻는다.

"우리도 Kubernetes 써야 하나요? Netflix는 Microservices라던데."

답: 사용자 100명이면 VPS 한 대로 충분하다. Netflix가 Microservices를 도입한 건 2억 명 사용자였고, 그 전엔 모놀리스였다. Shopify는 수십억 달러 GMV까지 Rails 모놀리스였다. Stack Overflow는 수천만 방문자를 서버 9대로 처리한다.

스케일링은 "미래의 100만 사용자"를 위한 게 아니다. 지금 막히는 병목을 푸는 것이다.

이 글은 사용자 수 변곡점별로:

  1. 무엇이 깨지는가 (구체적 병목)
  2. 어떻게 푸는가 (패턴과 트레이드오프)
  3. 언제 해야 하는가 (너무 일찍 vs 너무 늦게)
  4. 실제 회사들의 사례 (Stack Overflow, Shopify, Discord, Figma, Instagram)

을 한 번에 꿰어서 다룬다. Season 3 Episode 3이자, 지난 글 "유명 포스트모템 해부"에서 다뤘던 실패 사례들이 결국 "스케일링 변곡점을 잘못 다뤘을 때" 터진다는 교훈의 연장선이다.


Chapter 1: 100명 — 단일 서버의 시대

1.1 아키텍처

[사용자]
[DNSVPS 1]
[Nginx + App + SQLite + Redis 모두 한 박스]

1.2 왜 이게 충분한가

100명이 동시에 접속해도 초당 요청 수(RPS)는 고작 10~50 정도다. 최신 노트북조차 수천 RPS를 처리한다. 병목은 사용자 수가 아니라 쿼리 품질이다.

"Premature optimization is the root of all evil." — Donald Knuth

1.3 하지 말아야 할 것들

1.4 실제 사례 — Pieter Levels의 Nomad List

Pieter Levels는 혼자서 월 $100K 매출의 SaaS를 운영한다. 구성:

복잡도를 거부한 덕에 혼자 10개 제품을 동시에 유지한다. "보링한 기술(Boring Technology)"의 힘.


Chapter 2: 1,000명 — 수직 확장의 황금기

2.1 깨지는 것

2.2 해결책: 분리와 수직 확장

[사용자]
[Cloudflare (CDN + DNS)]
[App 서버 1] ── [DB 서버 1]
              [Daily BackupS3]

2.3 수직 확장의 미덕

"수평 확장을 고민하기 전에, 수직 확장의 한계를 알아야 한다."

AWS r7i.48xlarge는 192 vCPU, 1,536GB RAM을 제공한다. 대부분의 스타트업은 이 크기에 도달하기 전에 팔리거나 망한다.

2.4 이 단계에서 해야 할 것


Chapter 3: 10,000명 — 캐시와 읽기 복제본의 시대

3.1 깨지는 것

3.2 해결책: Read Replica + Redis + 로드 밸런서

[사용자]
[CDN: Cloudflare/Fastly]
[Load Balancer: ALB/Cloudflare]
[App x 2~4]
[Redis (세션/캐시)][Postgres Primary] ──replication──→ [Replica x 1~2]

3.3 Read Replica 전략

# Django 예시
DATABASES = {
    'default': {'HOST': 'primary.db', ...},
    'replica': {'HOST': 'replica.db', ...},
}

class ReadReplicaRouter:
    def db_for_read(self, model, **hints):
        return 'replica'
    def db_for_write(self, model, **hints):
        return 'default'

주의: Read-Your-Write 문제

해결:

  1. Session stickiness: 쓰기 직후 N초간 Primary 읽기
  2. Causal consistency: 쓰기 LSN(Log Sequence Number) 추적, Replica가 해당 LSN까지 올라올 때까지 대기

3.4 Redis 캐싱 패턴 3가지

Cache-aside (가장 흔함):

def get_user(user_id):
    cached = redis.get(f'user:{user_id}')
    if cached:
        return json.loads(cached)
    user = db.query('SELECT * FROM users WHERE id = %s', user_id)
    redis.setex(f'user:{user_id}', 300, json.dumps(user))
    return user

Write-through: 쓰기 시 DB + Cache 동시 업데이트. 일관성 높음, 쓰기 레이턴시 증가.

Write-behind: Cache에 쓰고 비동기로 DB에 쓰기. 빠름, but Redis 죽으면 데이터 손실 위험.

3.5 캐시 함정 3가지

  1. Thundering Herd: 인기 키 만료되는 순간 1000개 요청이 동시에 DB 때림 → 분산 락 또는 "확률적 early expiration"
  2. Cache Stampede: TTL 만료 → 재계산 비용 큰 쿼리 → 줄줄이 대기
  3. Cache Invalidation: "2 hard things in CS: naming things, cache invalidation, and off-by-one errors"

3.6 실제 사례 — Stack Overflow

Stack Overflow는 월 13억 페이지뷰를 서버 9대로 처리한다 (2023년 기준):

비결:

  1. 과격한 캐싱: 거의 모든 페이지 전체 캐싱
  2. C#과 튜닝: JIT로 컴파일, 수동 최적화
  3. SQL 서버 의존: 스토어드 프로시저, 인덱스 최적화
  4. 모놀리스 고수: Microservices 거부

교훈: 기술 선택보다 "잘하는 것"이 중요하다.


Chapter 4: 100,000명 — Write 확장의 시작

4.1 깨지는 것

4.2 해결책 1: Queue 기반 비동기화

[User Request]
[Web][SQS/Redis Queue]Pop[Worker x N]
  ↓                                    ↓
"접수됨" 즉시 응답                    [DB Write]

대상 작업:

4.3 해결책 2: CQRS (Command Query Responsibility Segregation)

예: 전자상거래

4.4 해결책 3: Connection Pooling

PostgreSQL은 각 연결이 OS 프로세스. 1만 연결 = 1만 프로세스 = 메모리 고갈.

PgBouncer 도입:

[App x 100대 × 100 conn = 10,000 conn]
[PgBouncer: 500 실제 연결만 유지]
[Postgres (500 backend)]

모드:

4.5 해결책 4: Multi-AZ

RDS Multi-AZ는 자동으로 이걸 해준다. Failover 30~60초.

4.6 실제 사례 — Shopify의 Pod 아키텍처

Shopify는 2020년경 "Pod" 아키텍처로 전환:

이건 샤딩의 한 형태지만, 중요한 건:

"We kept it a Rails monolith. Pods are operational boundaries, not service boundaries."

모놀리스 + 수평 파티셔닝 조합으로 Black Friday에 초당 수천만 요청 처리.


Chapter 5: 1,000,000명 — 샤딩의 시대

5.1 깨지는 것

5.2 샤딩 전략 3가지

1) Hash-based Sharding:

shard_id = hash(user_id) % N

2) Range-based Sharding:

user_id 1~1M → shard_1
user_id 1M~2M → shard_2

3) Directory-based (Lookup) Sharding:

lookup_table: user_id → shard_id

5.3 샤드 키 선택의 예술

좋은 샤드 키:

나쁜 샤드 키:

5.4 Cross-shard 쿼리 지옥

-- 샤딩 전
SELECT COUNT(*) FROM orders WHERE status = 'pending';

-- 샤딩 후: 모든 shard에 fan-out
SELECT COUNT(*) FROM shard_1.orders WHERE status = 'pending'
UNION ALL
SELECT COUNT(*) FROM shard_2.orders WHERE status = 'pending'
...
-- 집계 로직은 애플리케이션 레이어에서

JOIN across shards는 거의 불가능. 설계 초기에:

5.5 실제 사례 — Discord의 Cassandra → ScyllaDB

Discord는 초당 수백만 메시지를 다룬다. 2017년 Cassandra 도입:

Partition key: (channel_id, bucket) where bucket = timestamp / 2주

→ 한 채널 메시지가 여러 partition에 분산. Hot partition 회피.

트레이드오프: 채팅 검색이 어려워짐 (Elasticsearch 별도).


Chapter 6: 10,000,000명 — 멀티 리전과 엣지

6.1 깨지는 것

6.2 전략 1: CDN + 엣지 컴퓨팅

[사용자, 도쿄][Cloudflare 엣지, 도쿄]  (cache hit) 1ms
                       (cache miss)
               [Origin, 미국] 150ms

Cloudflare Workers, Vercel Edge Functions, AWS Lambda@Edge:

6.3 전략 2: Multi-region Active-Active

문제: 동일 사용자의 쓰기가 여러 리전에서 발생 → 충돌 해결 필요

해결책:

6.4 전략 3: 글로벌 분산 DB

6.5 실제 사례 — Figma의 분할 점령

Figma는 2020년 기준 단일 Postgres를 썼다. 그러다 CPU 65%+ 도달 위기.

선택: Microservices로 완전 분해 (risky, 느림) vs 수평 파티셔닝 (incremental).

전략: "colo (co-location) by foreign key"

  1. 강하게 연결된 테이블은 같은 DB에
  2. 약하게 연결된 테이블은 분리
  3. 점진적으로 여러 Postgres로 쪼갬

결과: 무중단으로 9개 DB로 분산, 단일 장애점 제거.

교훈: 빅뱅 리팩토링 대신 점진적 분할. 각 단계마다 롤백 가능하게.


Chapter 7: Microservices는 언제?

7.1 Conway의 법칙

"조직은 자신의 의사소통 구조를 복제한 시스템을 설계한다." — Melvin Conway

팀이 10개면 서비스 10개. 팀이 2개면 서비스 2개로 충분.

7.2 Microservices 준비도 체크리스트

모놀리스로 먼저 해결해야 할 질문들:

답이 "No"가 많다면, 모놀리스 유지.

7.3 모놀리스 → Microservices 전환 패턴

Strangler Fig Pattern:

  1. 새 기능은 새 서비스로
  2. 기존 기능을 하나씩 이관
  3. 최종적으로 모놀리스는 빈 껍데기

Branch by Abstraction:

  1. 기존 로직 앞에 추상화 레이어
  2. 레이어 뒤에서 점진적 구현 교체

7.4 실제 사례 — Netflix의 7년

Netflix가 DVD 대여에서 스트리밍으로 전환하며 Microservices 도입:

7년! 하룻밤에 안 된다.


Chapter 8: 언어 바꾸기 — 언제?

8.1 언어 교체의 진짜 이유

흔한 오해: "Python이 느려서 Go로 바꿨다."

실제: CPU가 병목인 경우는 드물다. 대부분 병목은:

8.2 언어 교체가 정당화되는 경우

  1. 동시성이 진짜 병목: Python GIL 때문에 멀티코어 못 씀 → Go/Rust
  2. 메모리가 진짜 병목: Ruby/Python 메모리 2~3배 → Go (1/3)
  3. 배포 단순화: Ruby 런타임 + gem 지옥 → Go 단일 바이너리
  4. 팀 역량: 대부분의 개발자가 해당 언어 숙련

8.3 실제 사례 — Twitter의 Scala

Twitter 초기: Ruby on Rails. "Fail Whale"로 유명.

2009년: 백엔드를 JVM 기반 Scala로 전환.

이유:

주의: 프론트엔드/웹은 한동안 Rails 유지. "적재적소(right tool for the job)".

8.4 실제 사례 — Dropbox의 Go

Dropbox 초기: Python.

2014년: 일부 시스템을 Go로 재작성.

재작성한 것: 동기화 엔진(Sync engine), 성능 크리티컬 코드.

유지한 것: 웹, API 대부분은 여전히 Python.

교훈: "전체 재작성"이 아니라 "핫패스만 재작성".

8.5 Magic Pocket — Dropbox의 Go 스토리지

Dropbox는 2016년 Amazon S3에서 자체 스토리지로 이전 — Magic Pocket 프로젝트:

Go 선택 이유:


Chapter 9: 데이터 계층의 10배 성장

9.1 1GB → 10GB

단일 Postgres에서 편안. 인덱스 관리, VACUUM 주의.

9.2 10GB → 100GB

9.3 100GB → 1TB

9.4 1TB → 10TB

9.5 10TB → 100TB

9.6 실제 사례 — GitHub의 Vitess

GitHub는 MySQL 위에서 돌아간다. 규모가 커지자 Vitess 도입:

교훈: "샤딩을 애플리케이션 코드에 스며들게 하지 말라. 게이트웨이로 추상화."


Chapter 10: 조직의 스케일링

10.1 팀 크기별 원칙

1~10명: 모두가 모든 코드 이해. 소통 비용 적음. Microservices 금지.

10~50명: 팀 분리 시작 (Frontend, Backend, Platform). Monorepo + 모듈 분리.

50~200명: Service ownership 명확화. Platform team이 공통 인프라 담당.

200~1000명: Organizational boundary = Service boundary. 전담 SRE, Security, DBA.

1000명+: 회사 안에 회사. 내부 플랫폼이 오픈소스화되기도 (Google Borg → Kubernetes).

10.2 Amazon의 Two-Pizza Rule

"팀은 피자 2판으로 먹일 수 있는 크기여야 한다." — Jeff Bezos

약 6~10명. 이 크기에서 소통 비용 최소화. 각 팀은 풀스택 오너십.

10.3 Spotify Model의 오해

"Spotify Model" (Squad, Tribe, Chapter, Guild)은 유명하지만:

  1. Spotify 자신조차 공식적으로 쓰지 않는다고 2020년 밝힘
  2. "Model"이 아니라 그 순간의 "스냅샷"이었음
  3. 무비판적 복사는 위험

교훈: 다른 회사의 조직 구조는 참고만. 본인 회사에 맞는 걸 실험.


Chapter 11: Observability의 스케일링

11.1 로그의 진화

100명: grep 10K명: CloudWatch Logs, Papertrail 1M명: ELK (Elasticsearch + Logstash + Kibana) 10M명: Loki, ClickHouse 기반 자체 구축 (비용)

11.2 메트릭의 진화

100명: Cloudwatch 기본 10K명: Prometheus + Grafana 1M명: Thanos/Cortex로 Prometheus 수평 확장 10M명: M3DB, VictoriaMetrics

11.3 Trace의 진화

100명: 안 씀 10K명: Sentry 같은 APM 1M명: Datadog APM, New Relic 10M명: OpenTelemetry + Tempo/Jaeger, 샘플링 전략 필수

11.4 비용 폭발 주의

Datadog은 사용자 규모 커지면 월 $100K+ 쉽게 간다. Coinbase는 2022년 월 $65M Datadog 비용 썼다고 알려짐.

완화:


Chapter 12: 스케일링 변곡점 체크리스트 (12항목)


Chapter 13: 10가지 안티패턴

1) 조기 Microservices 분리

팀 5명인데 서비스 10개. 각자 5개씩 운영. 배포만으로 하루 끝. 모놀리스부터.

2) 과도한 추상화

미래의 교체 가능성을 대비해 모든 의존성에 인터페이스. YAGNI (You Aren't Gonna Need It).

3) Resume-driven Development

"이력서에 쓰려고" Kafka, Kubernetes, Microservices 도입. 팀 과제가 아니라 개인 과제.

4) Big Bang Rewrite

전체 코드베이스 2년에 걸쳐 재작성. 끝났을 땐 시장이 바뀌어 있음. Strangler Fig 사용.

5) Cache만 추가하면 해결된다는 착각

캐시는 일관성 문제, Stampede 문제, 무효화 문제를 덤으로 준다. 캐시 없이 먼저 튜닝.

6) Premature Sharding

사용자 1만에 샤딩 시작. Cross-shard 쿼리 지옥. 수직 확장의 끝까지 가라.

7) 3-tier 흉내내기

"DB — Backend — Frontend 3 tier"만 있으면 확장성 있다는 미신. 실제 병목은 측정해야 안다.

8) "It works on my machine"

로컬에선 SQLite, 프로덕션은 Postgres. 로컬 Mongo 3.4, 프로덕션 Mongo 7.0. Dev/Prod parity (12-factor).

9) Observability 후순위

"바빠서 로그/메트릭은 나중에." 장애 나면 맨눈으로 디버깅. Day 1부터 관측성.

10) 비용 무관심

AWS 청구서를 아무도 안 본다. 월 $50K가 $200K 됨. FinOps는 Day 1.


마치며 — 스케일링의 진짜 원칙

원칙 1: 측정 먼저

추측으로 최적화하지 말라. 프로파일링, APM, 로그 보고 진짜 병목 확인.

원칙 2: 충분히 늦게, 충분히 일찍

원칙 3: 가장 지루한 솔루션 먼저

Postgres, Redis, Linux, Nginx. 50년 된 기술들이 최신 "혁신" 대부분을 이긴다.

원칙 4: 점진적 진화

빅뱅은 실패율이 높다. 각 단계마다 롤백 가능하게.

원칙 5: 스케일링은 기술만이 아니다

조직, 프로세스, 문화가 함께 스케일해야 한다. Conway의 법칙을 기억하라.

원칙 6: 원본을 읽어라


다음 글 예고 — "빅테크 개발 문화 완전 가이드: Google, Meta, Amazon, Netflix의 차이"

Season 3 Ep 4는:

다음 글에서.

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다