Guider/Backend/BackendDevGuide0013
Backend#13

BackendDevGuide0013

대규모 시스템 설계와 분산 아키텍처 완전 정복

🏗️
BackendDevGuide · Episode 13
🏗️⚡

대규모 시스템 설계와
분산 아키텍처 완전 정복

A부터 Z까지 — 수천만 사용자를 견디는 서비스를 설계하는 법

⚖️ 로드밸런서 💾 캐시 전략 🗄️ 분산 DB 📨 메시지 큐 🔩 MSA 🛡️ 장애 대응
📚
14
핵심 챕터
💻
70+
설계 패턴/코드
🎯
100M+
MAU 수준 설계
A-Z
완전 가이드

📋 전체 학습 목차

⚖️ Ch01 · 대규모 시스템 설계 프레임워크
🌐 Ch02 · DNS와 CDN — 글로벌 트래픽 분산
⚡ Ch03 · 로드 밸런싱 완전 정복
💾 Ch04 · 캐싱 전략 — Redis 심화
🗄️ Ch05 · 데이터베이스 스케일링 전략
🔀 Ch06 · 데이터 파티셔닝과 샤딩
📨 Ch07 · 메시지 큐와 이벤트 드리븐
🔩 Ch08 · 마이크로서비스 아키텍처 패턴
🛡️ Ch09 · Circuit Breaker와 장애 격리
🔍 Ch10 · 서비스 디스커버리와 API Gateway
📊 Ch11 · 분산 추적과 관찰가능성
🔐 Ch12 · 분산 시스템 일관성과 CAP 이론
📐 Ch13 · 실전 시스템 설계 면접 — 인스타그램/유튜브
🎯 Ch14 · 면접 Q&A + 실무 아키텍처 체크리스트

⚖️ Chapter 01

대규모 시스템 설계 프레임워크 — 어디서부터 시작할까?

"면접에서도, 현업에서도 통하는 시스템 설계 4단계 접근법"

🗺️ 규모 추정 치트시트 — 암기 필수!

시스템 설계 면접에서 규모 추정(Back of the envelope estimation)은 필수입니다. 이 숫자들을 외워두면 어떤 면접에서도 자신있게 답할 수 있습니다.

⏱️ 레이턴시 기준값

작업 시간
L1 캐시 접근 0.5 ns
메모리(RAM) 접근 100 ns
SSD 랜덤 읽기 150 µs
HDD 탐색 10 ms
같은 DC 네트워크 왕복 0.5 ms
서울↔미국 네트워크 150 ms

📊 규모 단위 기준

단위 예시
1 KB 10³ bytes 텍스트 1페이지
1 MB 10⁶ bytes 사진 1장
1 GB 10⁹ bytes 영화 1편
1 TB 10¹² bytes 영화 1,000편
초당 요청(QPS) DAU × 평균요청 / 86400 -
하루=86,400초 ≈ 10만 초 암기!

🔢 실전 규모 추정 예제 — 트위터 클론

# 가정
DAU = 3억 명
트윗 읽기 : 쓰기 = 100 : 1
사용자 1명이 하루 평균 트윗 1개 작성, 읽기 100개

# QPS 계산
쓰기 QPS = 3억 / 86400 ≈ 3,500 QPS
읽기 QPS = 3,500 × 100 = 350,000 QPS  # 피크는 2배 = 700,000 QPS

# 저장 용량 계산 (5년)
트윗 1개 용량 = 텍스트 300B + 메타데이터 200B = 500B
하루 새 트윗 = 3억 개
5년 총 트윗 = 3억 × 365 × 5 = 5,475억 개
5년 저장 공간 = 5,475억 × 500B ≈ 274 TB

# 미디어 저장 (트윗 중 10%가 이미지, 평균 1MB)
하루 미디어 = 3억 × 0.1 × 1MB = 30 TB/일
5년 미디어 = 30 × 365 × 5 ≈ 54 PB

# 결론: 어떤 아키텍처가 필요한가?
- DB 읽기 부하: 캐시(Redis) 필수
- 쓰기 부하: DB 파티셔닝/샤딩 필요
- 미디어: CDN + 분산 스토리지(S3)
- 팔로워 피드: 팬아웃 전략 필요

🌐 Chapter 02

DNS와 CDN — 글로벌 트래픽 분산의 핵심

"서울 서버 하나로 뉴욕 사용자에게 0.1초 응답? CDN이 해냅니다"

🌍 CDN(Content Delivery Network) 완전 이해

CDN은 전 세계 각지에 엣지 서버(Edge Server)를 두고, 사용자에게 가장 가까운 서버에서 콘텐츠를 제공합니다. 마치 전국 각지에 편의점(엣지 서버)이 있어서, 본사(오리진 서버)까지 가지 않아도 되는 것과 같습니다.

🖼️
정적 CDN
이미지, CSS, JS, 폰트
변경 없는 파일 캐싱
CloudFront, Cloudflare
🎬
동적 CDN
API 응답 캐싱
Edge Computing
Cloudflare Workers
🎥
스트리밍 CDN
HLS/DASH 비디오
실시간 스트리밍
AWS MediaPackage
# CDN 캐시 전략 — Cache-Control 헤더

// NestJS에서 정적 파일 캐시 설정
@Get('image/:id')
@Header('Cache-Control', 'public, max-age=86400, s-maxage=604800')
// max-age=86400     → 브라우저 1일 캐시
// s-maxage=604800   → CDN 7일 캐시 (CDN 전용)
getImage(@Param('id') id: string) { ... }

// API 응답은 캐시하지 않음 (실시간 데이터)
@Header('Cache-Control', 'no-store')

// 버전 관리된 파일은 영구 캐시
// main.a3b4c5.js → 파일명에 해시 포함
@Header('Cache-Control', 'public, max-age=31536000, immutable')
// immutable → 만료 전 재검증 요청 안 함 (성능 최적화)

⚡ Chapter 03

로드 밸런싱 완전 정복 — 트래픽을 고르게 나누는 법

"식당 입구에서 빈 테이블로 안내하는 직원이 바로 로드밸런서입니다"

🍽️ 로드밸런서를 식당으로 이해하기

인기 식당에 손님이 몰리면 어떻게 하나요? 여러 개의 테이블(서버)을 준비하고, 입구 직원(로드밸런서)이 빈 테이블로 안내합니다. 단, 어떤 테이블로 안내하느냐에는 다양한 전략이 있습니다.

알고리즘 방식 적합 상황 단점
Round Robin 순서대로 1→2→3→1... 서버 성능이 동일할 때 서버 부하 무시
Weighted RR 가중치에 따라 분배 서버 성능 다를 때 설정 관리 필요
Least Connections 연결 수 가장 적은 서버 요청 처리 시간 다를 때 연결 수 추적 오버헤드
IP Hash IP 기반 특정 서버 고정 세션 유지 필요 시 서버 추가 시 재매핑
Least Response Time 응답 시간 가장 짧은 서버 응답 시간 중요 시 구현 복잡
# Nginx 로드밸런서 설정 — 실전

upstream api_servers {
    # Least Connections 알고리즘
    least_conn;

    server api1.internal:3000 weight=3;  # 성능 좋은 서버 3배 더
    server api2.internal:3000 weight=2;
    server api3.internal:3000 weight=1;

    # 헬스체크: 3번 실패하면 제외, 10초마다 재시도
    server api4.internal:3000 backup;    # 나머지 다 죽으면 사용

    # Keep-Alive 커넥션 풀 (성능 최적화)
    keepalive 32;
}

server {
    listen 80;

    location /api/ {
        proxy_pass http://api_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";       # keep-alive 활성화
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 서버 장애 시 다른 서버로 자동 재시도
        proxy_next_upstream error timeout http_500 http_502 http_503;
        proxy_next_upstream_tries 3;

        # 타임아웃 설정
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
    }
}

🏗️ L4 vs L7 로드밸런서 비교

L4 (Transport Layer)

TCP/UDP 레벨에서 동작
IP + Port 기반 라우팅
• 빠름 (패킷 내용 안 봄)
• HTTP 헤더/URL 모름
• AWS NLB
• 게임 서버, IoT에 적합

L7 (Application Layer)

HTTP/HTTPS 레벨에서 동작
URL, 헤더, 쿠키 기반 라우팅
• SSL Termination
• 경로 기반 라우팅 가능
• AWS ALB, Nginx
• 웹 API, MSA에 적합

💾 Chapter 04

캐싱 전략 완전 정복 — Redis 심화

"캐시는 마법이 아닙니다. 어디에 무엇을 얼마나 캐시할지가 핵심입니다"

🗺️ 캐시 계층 구조 — 어디에 캐시를 둘까?

💻
1. 브라우저
HTTP Cache
0ms
🌍
2. CDN
엣지 캐시
~10ms
⚖️
3. LB
Nginx 캐시
~5ms
🔴
4. Redis
앱 캐시
~1ms
🗄️
5. DB
원본 데이터
~50ms

🔄 캐시 패턴 4종 완전 이해

패턴 흐름 장점 단점 사용처
Cache-Aside 앱→캐시미스→DB→캐시저장 구현 쉬움 Cold Start 읽기 많은 API
Write-Through DB쓰기+캐시쓰기 동시 일관성 보장 쓰기 지연 금융 데이터
Write-Behind 캐시 먼저→DB 비동기 쓰기 빠름 데이터 유실 위험 로그, 조회수
Read-Through 캐시가 DB 자동 조회 앱 로직 단순 첫 요청 느림 ORM 캐시

💻 Redis 고급 패턴 — 실전 NestJS 구현

// Redis 고급 활용 패턴 (NestJS)
// 1. 분산 락 (Distributed Lock) — 동시성 제어
@Injectable()
export class RedisLockService {
  async acquireLock(key: string, ttlMs: number = 5000): Promise<boolean> {
    // SET key value NX PX ttl (존재하지 않을 때만 SET)
    const result = await this.redis.set(
      `lock:${key}`,
      '1',
      'NX',   // Not eXists: 없을 때만
      'PX',   // 밀리초 단위 TTL
      ttlMs
    );
    return result === 'OK';
  }

  async releaseLock(key: string) {
    await this.redis.del(`lock:${key}`);
  }

  // 사용 예: 중복 결제 방지
  async processPayment(orderId: string) {
    const locked = await this.acquireLock(`payment:${orderId}`);
    if (!locked) throw new Error('이미 처리 중인 주문입니다');
    try {
      // 결제 처리 로직
    } finally {
      await this.releaseLock(`payment:${orderId}`);
    }
  }
}

// 2. 캐시 스탬피드(thundering herd) 방지 — 동시 캐시 미스 처리
async getWithLock(key: string, fetchFn: () => Promise<any>) {
  let data = await this.redis.get(key);
  if (data) return JSON.parse(data);

  // 캐시 미스 시 한 프로세스만 DB 조회하도록 락
  const lockKey = `lock:${key}`;
  const locked = await this.redis.set(lockKey, '1', 'NX', 'PX', 10000);
  
  if (!locked) {
    // 다른 프로세스가 채우는 중 → 잠시 대기 후 재시도
    await sleep(100);
    return this.getWithLock(key, fetchFn);
  }

  const freshData = await fetchFn();
  await this.redis.setex(key, 3600, JSON.stringify(freshData));
  await this.redis.del(lockKey);
  return freshData;
}

// 3. Sliding Window Rate Limiter (슬라이딩 윈도우)
async isRateLimited(userId: string, limit = 100, windowSec = 60): Promise<boolean> {
  const key = `ratelimit:${userId}`;
  const now = Date.now();
  const windowStart = now - windowSec * 1000;

  // Lua 스크립트로 원자적 처리
  const script = `
    redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])
    local count = redis.call('ZCARD', KEYS[1])
    if count < tonumber(ARGV[2]) then
      redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])
      redis.call('EXPIRE', KEYS[1], ARGV[4])
      return 0
    end
    return 1
  `;
  
  const result = await this.redis.eval(script, 1, key, windowStart, limit, now, windowSec);
  return result === 1;  // 1 = 제한됨
}

🗄️ Chapter 05

데이터베이스 스케일링 전략 — 1억 건 데이터 다루기

"DB 스케일링을 모르면 서비스 성장의 발목이 잡힙니다"

📈 DB 스케일링 전략 비교

⬆️ Scale-Up (수직 확장)

더 강력한 서버로 교체
CPU, RAM, SSD 업그레이드
• 구현 간단
• 단일 장애점(SPOF)
• 한계가 있음 (물리적 한계)
• 고비용

↔️ Scale-Out (수평 확장)

여러 서버로 부하 분산
읽기: Read Replica
쓰기: Sharding
• 이론상 무한 확장
• 복잡한 구현
• 분산 트랜잭션 어려움

📖 Read Replica — 읽기 부하 분산

전형적인 웹 서비스는 읽기:쓰기 = 80:20 또는 90:10 비율입니다. Master DB는 쓰기만 담당하고, 여러 Replica가 읽기를 나눠 처리합니다.

// TypeORM Read Replica 설정
TypeOrmModule.forRoot({
  type: 'postgres',
  replication: {
    master: {
      host: 'primary.db.internal',   // 쓰기 전용
      port: 5432,
      database: 'mydb',
      username: 'user',
      password: 'pass',
    },
    slaves: [
      { host: 'replica1.db.internal', port: 5432, database: 'mydb', username: 'user', password: 'pass' },
      { host: 'replica2.db.internal', port: 5432, database: 'mydb', username: 'user', password: 'pass' },
    ],
  },
  // TypeORM이 SELECT는 자동으로 replica에서 실행!
  // INSERT/UPDATE/DELETE는 master에서 실행
})

// 주의: 복제 지연(Replication Lag)!
// 쓰기 직후 읽기 시 최신 데이터가 없을 수 있음
// 해결: 방금 쓴 데이터는 Master에서 읽기 (강제 지정)
const masterEntityManager = dataSource.createEntityManager({ replication: { primary: true } });

🔀 Chapter 06

데이터베이스 파티셔닝과 샤딩

"1억 건이 넘는 데이터를 하나의 테이블에 넣으면 안 됩니다"

📂 파티셔닝 vs 샤딩 차이

📑 파티셔닝 (같은 DB 서버)

하나의 테이블을 논리적으로 분할
• Range 파티션: 날짜별 분리
• List 파티션: 지역별 분리
• Hash 파티션: 균등 분배

예: orders 테이블을 월별로 분리
orders_2024_01, orders_2024_02...

🔪 샤딩 (다른 DB 서버)

데이터를 여러 DB 서버로 분산
• User ID 기반 샤딩
• 지역 기반 샤딩
• 일관된 해싱(Consistent Hashing)

예: shard0: userId % 3 = 0
shard1: userId % 3 = 1
shard2: userId % 3 = 2

// 샤딩 라우터 구현 예시
@Injectable()
export class ShardRouter {
  private shards: DataSource[];

  getShardForUser(userId: string): DataSource {
    // 일관된 해싱으로 샤드 결정
    const hash = this.consistentHash(userId);
    const shardIndex = hash % this.shards.length;
    return this.shards[shardIndex];
  }

  // ⚠️ 샤딩의 주요 문제점
  // 1. 크로스-샤드 JOIN 불가 → 앱 레벨에서 처리
  // 2. 샤드 추가 시 데이터 재분배 (리샤딩) 비용
  // 3. 분산 트랜잭션 매우 복잡
  // → 일관된 해싱으로 리샤딩 최소화
}

📨 Chapter 07

메시지 큐와 이벤트 드리븐 아키텍처

"결합도를 낮추고 확장성을 높이는 비동기 메시지 처리"

📮 메시지 큐를 우체국으로 이해하기

메시지 큐는 우체국과 같습니다. 보내는 사람(Producer)이 편지(메시지)를 우체통(큐)에 넣으면, 받는 사람(Consumer)이 편의에 맞게 꺼내 처리합니다. 서로 동시에 연결할 필요가 없습니다.

비교 Kafka RabbitMQ AWS SQS
처리량 100만+/초 수만/초 수천~수만/초
메시지 보관 영구 (설정 기간) 처리 후 삭제 14일
순서 보장 파티션 내 보장 큐 단위 보장 FIFO 큐 사용
재처리 오프셋으로 재처리 DLQ로 이동 DLQ 설정
적합 사용처 이벤트 스트리밍, 로그 작업 큐, RPC 서버리스, AWS 통합
// NestJS + Kafka 이벤트 드리븐 구현

// 1. Producer — 주문 이벤트 발행
@Injectable()
export class OrderService {
  constructor(@Inject('KAFKA_CLIENT') private kafka: ClientKafka) {}

  async createOrder(dto: CreateOrderDto) {
    const order = await this.orderRepo.save(dto);

    // 이벤트 발행 (비동기, 다른 서비스가 처리)
    await this.kafka.emit('order.created', {
      orderId: order.id,
      userId: order.userId,
      items: order.items,
      totalAmount: order.totalAmount,
      createdAt: order.createdAt,
    });

    // 주문 서비스는 여기서 완료! 결제/알림/재고는 각 서비스가 처리
    return order;
  }
}

// 2. Consumer — 결제 서비스 (order.created 이벤트 소비)
@Controller()
export class PaymentConsumer {
  @MessagePattern('order.created')
  async handleOrderCreated(
    @Payload() data: OrderCreatedEvent,
    @Ctx() context: KafkaContext,
  ) {
    try {
      await this.paymentService.processPayment(data.orderId, data.totalAmount);
      context.getMessage().commit();  // 성공 시 오프셋 커밋
    } catch (err) {
      // 실패 시 DLQ(Dead Letter Queue)로 이동
      await this.kafka.emit('order.payment.failed', data);
    }
  }
}

// 3. 알림 서비스도 동일한 이벤트 구독 (서비스 간 결합도 없음!)
@MessagePattern('order.created')
async handleOrderCreated(data: OrderCreatedEvent) {
  await this.notificationService.sendOrderConfirmation(data.userId, data.orderId);
}

🔩 Chapter 08

마이크로서비스 아키텍처 패턴 — MSA 설계의 모든 것

"모놀리스에서 MSA로 넘어갈 때 알아야 할 핵심 패턴들"

🏗️ 모놀리스 vs MSA — 언제 MSA로 가야 하나?

구분 모놀리스 MSA
초기 개발 속도 ✅ 빠름 ❌ 느림 (인프라 복잡)
독립 배포 ❌ 전체 재배포 ✅ 서비스별 독립
장애 격리 ❌ 전체 영향 ✅ 일부만 영향
스케일링 ❌ 전체 스케일 ✅ 서비스별 스케일
데이터 관리 ✅ 단일 DB (JOIN 쉬움) ❌ 서비스별 DB (JOIN 어려움)
운영 복잡도 ✅ 낮음 ❌ 높음 (분산 트레이싱 필요)
추천 시점 초기, 팀 3~5명 팀 50명+, 서비스 복잡할 때
💡 Martin Fowler의 조언: "모놀리스로 시작하고, 서비스 경계가 명확해지면 그때 MSA로 전환하세요. 처음부터 MSA는 대부분 실패합니다."

🎯 MSA 핵심 패턴 6가지

🚪
API Gateway
클라이언트 단일 진입점. 라우팅, 인증, 로드밸런싱, 로깅 통합
🗃️
DB per Service
서비스마다 독립 DB. 느슨한 결합. 서비스별 최적 DB 선택
📡
Event Sourcing
상태 변경을 이벤트로 저장. 감사 로그, 재처리, 타임트래블 가능
📋
SAGA Pattern
분산 트랜잭션 처리. 단계별 실행, 실패 시 보상 트랜잭션
🔄
CQRS
읽기/쓰기 모델 분리. 쓰기: 정규화, 읽기: 비정규화 최적화
🔌
Strangler Fig
모놀리스 → MSA 점진적 전환. 기능별로 하나씩 분리

💻 SAGA 패턴 — 분산 트랜잭션 실전 구현

// SAGA 패턴 — 주문-결제-배송 분산 트랜잭션
// Choreography 방식 (이벤트 기반)

// 정상 흐름
// 1. 주문 서비스: 주문 생성 → order.created 발행
// 2. 재고 서비스: order.created 수신 → 재고 감소 → inventory.reserved 발행
// 3. 결제 서비스: inventory.reserved 수신 → 결제 → payment.completed 발행
// 4. 배송 서비스: payment.completed 수신 → 배송 시작

// 실패 흐름 (보상 트랜잭션)
// 3. 결제 실패 → payment.failed 발행
// 2. 재고 서비스: payment.failed 수신 → 재고 복원 (보상)
// 1. 주문 서비스: inventory.release 수신 → 주문 취소 (보상)

@MessagePattern('payment.failed')
async handlePaymentFailed(event: PaymentFailedEvent) {
  // 보상 트랜잭션: 재고 복원
  await this.inventoryService.releaseReservation(event.orderId);
  // 주문 서비스에게 알림
  await this.kafka.emit('inventory.released', { orderId: event.orderId });
}

// CQRS 패턴 — 읽기/쓰기 모델 분리
// Write Model (Command): 정규화된 DB (MySQL)
export class CreateProductCommand {
  constructor(public readonly name: string, public readonly price: number) {}
}

// Read Model (Query): 비정규화된 ElasticSearch (빠른 검색)
// 이벤트를 통해 Write→Read 동기화
@EventsHandler(ProductCreatedEvent)
export class ProductCreatedHandler {
  async handle(event: ProductCreatedEvent) {
    // ES에 읽기 최적화된 형태로 저장
    await this.elasticsearchService.index({
      index: 'products',
      id: event.productId,
      document: {
        name: event.name,
        price: event.price,
        searchText: `${event.name} ${event.category}`,
      }
    });
  }
}

🛡️ Chapter 09

Circuit Breaker와 장애 격리 패턴

"한 서비스의 장애가 전체를 무너뜨리지 않게 하는 방법"

💡 Circuit Breaker — 전기 회로 차단기로 이해하기

전기 차단기는 과전류 시 전기를 차단해 화재를 막습니다. 서킷 브레이커 패턴도 마찬가지입니다. 연결된 서비스가 계속 실패하면, 더 이상 요청을 보내지 않고 빠르게 실패(Fail Fast)해서 시스템을 보호합니다.

🔄 서킷 브레이커 3가지 상태
🟢
CLOSED
정상. 요청 통과.
에러율 모니터링
🔴
OPEN
차단. 즉시 실패.
fallback 실행
🟡
HALF-OPEN
시험 요청.
성공 시 CLOSED
// NestJS Circuit Breaker 구현
// npm install opossum

@Injectable()
export class PaymentService {
  private breaker: CircuitBreaker;

  constructor(private httpService: HttpService) {
    // 서킷 브레이커 설정
    this.breaker = new CircuitBreaker(
      async (amount: number) => {
        return this.httpService.post('http://payment-service/pay', { amount }).toPromise();
      },
      {
        timeout: 3000,           // 3초 내 응답 없으면 실패
        errorThresholdPercentage: 50,  // 50% 실패율 시 OPEN
        resetTimeout: 30000,     // 30초 후 HALF-OPEN으로
        volumeThreshold: 10,     // 최소 10번 요청 후 판단
      }
    );

    // fallback: 서킷 OPEN 시 대안 응답
    this.breaker.fallback((amount: number) => ({
      success: false,
      message: '결제 서비스 일시 장애. 잠시 후 다시 시도해주세요.',
      retryAfter: 30,
    }));

    // 상태 변화 이벤트
    this.breaker.on('open', () => this.logger.warn('🔴 Circuit OPEN: payment-service'));
    this.breaker.on('halfOpen', () => this.logger.log('🟡 Circuit HALF-OPEN'));
    this.breaker.on('close', () => this.logger.log('🟢 Circuit CLOSED: 복구됨'));
  }

  async pay(amount: number) {
    return this.breaker.fire(amount);  // 자동으로 상태 관리
  }
}

🔍 Chapter 10

서비스 디스커버리와 API Gateway

"수십 개의 마이크로서비스 주소를 어떻게 관리하나요?"

🔍 서비스 디스커버리 — 자동 주소록

MSA에서 서비스는 동적으로 생성·삭제됩니다. IP도 바뀝니다. 서비스 디스커버리는 이 주소를 자동으로 관리하는 자동 주소록입니다.

📱 Client-Side Discovery

클라이언트가 레지스트리에서 서비스 주소 조회
• Netflix Eureka
• etcd, Consul
클라이언트 로드밸런싱 로직 포함
→ 클라이언트 복잡도 증가

🖥️ Server-Side Discovery

클라이언트 → LB → 레지스트리 → 서비스
• AWS ELB + Route 53
• Kubernetes Service
클라이언트는 단순해짐
→ LB가 디스커버리 담당

# Kubernetes에서 서비스 디스커버리 — DNS 자동 등록
# Service 생성 시 자동으로 DNS 등록됨
# 서비스명.네임스페이스.svc.cluster.local

# 예시
payment-svc.production.svc.cluster.local:80
user-svc.production.svc.cluster.local:80

# NestJS에서 서비스 호출 (하드코딩 대신 DNS 사용)
// ❌ 하드코딩
http://10.0.1.45:3001/pay

// ✅ DNS 기반 (K8s 내부)
http://payment-svc/pay

// ✅ 환경변수로 관리
PAYMENT_SERVICE_URL=http://payment-svc
USER_SERVICE_URL=http://user-svc

📊 Chapter 11

분산 추적과 관찰가능성 — 장애 원인을 찾는 방법

"10개 서비스를 거친 요청에서 어디서 느려졌는지 어떻게 알까요?"

🔭 관찰가능성 3대 요소 (Observability Pillars)

📊
Metrics
숫자로 된 측정값
CPU, 메모리, QPS, 에러율
Prometheus + Grafana
📝
Logs
이벤트 기록
에러, 요청/응답
ELK Stack, Loki
🔍
Traces
요청 흐름 추적
서비스 간 이동 경로
Jaeger, Zipkin, Tempo
// OpenTelemetry — 분산 추적 표준 (NestJS)
// npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node

// tracing.ts — 앱 시작 전 초기화 필수
const sdk = new NodeSDK({
  serviceName: 'order-service',
  traceExporter: new OTLPTraceExporter({
    url: 'http://jaeger:4318/v1/traces',
  }),
  instrumentations: [
    getNodeAutoInstrumentations(),  // HTTP, Express, DB 자동 추적
  ],
});
sdk.start();

// Trace ID가 자동으로 모든 서비스로 전파됨
// 요청 헤더에 traceparent: 00-TRACE_ID-SPAN_ID-01 포함
//
// Jaeger UI에서 확인:
// api-gateway (10ms)
// └─ order-service (8ms)
// │  └─ DB query (5ms) ← 병목!
// └─ payment-service (3ms)

🔐 Chapter 12

분산 시스템 일관성과 CAP 이론

"분산 시스템의 근본적인 한계와 트레이드오프를 이해합니다"

📐 CAP 이론 완전 이해

CAP 이론: 분산 시스템에서 C, A, P 세 가지를 동시에 모두 보장할 수 없습니다. 반드시 하나를 포기해야 합니다.

📋
C — Consistency
일관성: 모든 노드가 같은 데이터를 봄. 은행 잔액처럼 항상 최신 값
⏱️
A — Availability
가용성: 모든 요청은 응답을 받음 (오류 없이). 느릴 수 있음
🌐
P — Partition Tolerance
분할 내성: 네트워크 분리 발생 시에도 동작. 분산 시스템에서 필수
🗂️ CAP별 DB 선택
타입 특성 대표 DB 사용처
CP 일관성 + 분할내성 MongoDB, HBase, ZooKeeper 금융, 재고, 분산 락
AP 가용성 + 분할내성 Cassandra, CouchDB, DynamoDB SNS 피드, 로그, 장바구니
CA 일관성 + 가용성 MySQL, PostgreSQL (단일 노드) 단일 서버 환경

⚖️ PACELC 이론 — CAP의 확장

CAP이 "네트워크 분할 시" 선택이라면, PACELC는 정상 상태에서도 Latency(지연) vs Consistency(일관성) 트레이드오프가 있다는 것을 추가로 설명합니다.

# PACELC = 네트워크 분할(Partition) 시: A vs C
#          정상(Else) 시: L(Latency) vs C(Consistency)

# 예시: 뱅킹 앱 (일관성 중시)
계좌 이체 API:
  → 쓰기: 모든 레플리카에 동기 복제 완료 후 응답
  → 지연 높음 (100ms+) but 항상 최신 데이터 보장

# 예시: SNS 좋아요 수 (가용성/지연 중시)
좋아요 카운트 API:
  → 쓰기: 하나만 쓰고 즉시 응답 (비동기 복제)
  → 지연 낮음 (10ms) but 잠시 다른 수가 보일 수 있음
  → Eventual Consistency (결국 일관성 달성)

# 결국 비즈니스 요구사항에 따라 결정!

📐 Chapter 13

실전 시스템 설계 면접 — 인스타그램/유튜브 클론

"실제 면접에서 나오는 설계 문제 2가지를 완전히 풀어드립니다"

📸 인스타그램 피드 시스템 설계

# 요구사항
DAU: 5억 명
사진/동영상 업로드: 하루 1억 건
피드 읽기: 하루 50억 건 (쓰기:읽기 = 1:50)

# 핵심 기능
1. 사진/동영상 업로드
2. 팔로워 피드 (최근 게시글)
3. 좋아요, 댓글

# 아키텍처 결정

미디어 저장:
  사진 → S3 + CloudFront CDN
  이미지 리사이징 → Lambda (썸네일 자동 생성)
  형식: WEBP 변환 (JPEG 대비 30% 작음)

피드 생성 전략:
  팔로워 < 1만 명: Fan-out on Write
    → 게시 시 팔로워의 피드 테이블에 즉시 복사
    → 읽기 빠름, 쓰기 부하 높음
  팔로워 > 1만 명 (셀럽): Fan-out on Read
    → 피드 조회 시 팔로잉 목록 기반 조합
    → 쓰기 부하 없음, 읽기 복잡

데이터 모델:
  posts 테이블 (샤딩: user_id 기반)
  feeds 테이블 (Redis sorted set: 타임스탬프 score)
  users 테이블
  follows 테이블

캐싱:
  인기 계정 피드: Redis에 24시간 캐싱
  이미지 URL: CDN 캐싱 (7일)
  좋아요 수: Redis counter (Write-Behind)

🎬 유튜브 동영상 시스템 설계

# 핵심 설계 포인트

1. 동영상 업로드 플로우:
   클라이언트 → API Gateway
   → S3 원본 저장 (업로드 완료 이벤트)
   → Kafka: video.uploaded 이벤트 발행
   → 트랜스코딩 서비스 (비동기)
     → 다양한 해상도 생성 (240p/480p/720p/1080p/4K)
     → HLS 형식으로 변환 (스트리밍 최적화)
     → S3에 저장 → CloudFront CDN 배포
   → 메타데이터 DB (제목, 설명, 썸네일)
   → 검색 인덱싱 (ElasticSearch)

2. 동영상 스트리밍:
   HLS(HTTP Live Streaming) 사용
   → 동영상을 수 초 단위 청크(chunk)로 분할
   → 네트워크 속도에 따라 해상도 자동 변경
   → CDN에서 가장 가까운 엣지에서 서빙

3. 인기 동영상 최적화:
   상위 20% 인기 영상 = 80% 트래픽
   → 인기 영상: CDN에 더 많은 엣지 캐싱
   → 실시간 조회수: Redis 카운터 (배치로 DB 동기화)

4. 추천 시스템:
   협업 필터링 + 콘텐츠 기반 필터링 혼합
   → 사용자 시청 이력 → Kafka → 추천 엔진
   → 결과를 Redis에 캐싱 (1시간)

🎯 Chapter 14

면접 Q&A + 실무 아키텍처 체크리스트

"시스템 설계 면접에서 절대 틀리면 안 되는 질문들"

💬 시스템 설계 면접 핵심 Q&A

Q. 수평 확장 시 세션(Session)은 어떻게 관리하나요?
서버 여러 대에서 메모리 세션을 공유할 수 없습니다. 해결책 3가지: ①Sticky Session(같은 서버로 고정, 단일 장애점 존재), ②세션을 외부 저장소에 분리(Redis Cluster — 추천), ③JWT 같은 Stateless 토큰 사용. 현업에서는 Redis Session Store 또는 JWT를 주로 사용합니다.
Q. API 응답이 갑자기 느려졌습니다. 어떻게 디버깅하나요?
체계적인 접근: ①Grafana로 응답시간 스파이크 시간대 확인, ②동시간대 CPU/메모리/DB 커넥션 수 이상 여부 확인, ③Jaeger(분산 추적)로 어느 서비스에서 지연인지 특정, ④해당 서비스 로그에서 슬로우 쿼리/에러 확인, ⑤EXPLAIN ANALYZE로 쿼리 실행 계획 분석. 주요 원인: N+1 쿼리, 인덱스 누락, 캐시 히트율 저하, 외부 API 지연.
Q. 일관된 해싱(Consistent Hashing)이란?
일반 해싱은 서버 추가/제거 시 거의 모든 키가 재배치됩니다. 일관된 해싱은 가상 노드 링(ring)을 사용해 서버 변경 시 최소한의 키만 이동합니다. 서버 N개에서 N+1개로 늘릴 때 1/N+1 비율의 키만 재배치. 활용: Redis Cluster, Cassandra, 분산 캐시.
Q. 데이터베이스 인덱스의 종류와 선택 기준은?
① B-Tree 인덱스: 범위 쿼리, 정렬에 적합. 범용적. ② Hash 인덱스: 동등 비교만 가능(=연산). 범위 쿼리 불가. ③ Composite(복합) 인덱스: (A, B) 인덱스는 A 단독, A+B 조합 쿼리에 사용. B 단독엔 미사용. ④ Covering 인덱스: 쿼리에 필요한 모든 컬럼이 인덱스에 포함 → 테이블 접근 없음(최고 성능). 선택 기준: WHERE, JOIN ON, ORDER BY 컬럼에 적용, 카디널리티 높은 컬럼 우선.
Q. Kafka와 RabbitMQ 어떤 걸 선택하나요?
Kafka: 고처리량(100만+/초), 메시지 영구 보관, 재처리 가능, 실시간 스트리밍, 이벤트 소싱. → 대용량 로그, 이벤트 스트리밍, 마이크로서비스 이벤트 버스.
RabbitMQ: 복잡한 라우팅(토픽, 헤더), 빠른 설정, 오래된 생태계, 메시지 우선순위, RPC 패턴. → 작업 큐, 마이크로서비스 RPC, 복잡한 라우팅 필요 시.
Q. 10억 건의 사용자 데이터에서 이메일 중복 검사를 빠르게 하려면?
방법들: ① email 컬럼에 UNIQUE 인덱스 + DB 제약조건 (가장 기본). ② 가입 시 Redis SETNX로 이메일 선점 (DB 부하 감소). ③ Bloom Filter: 확률적 자료구조로 "없음"을 확실히 판단. 있을 수 있다고 판단 시에만 DB 조회 (거짓 양성 가능하지만 거짓 음성 없음). Redis에서 BF.ADD/BF.EXISTS 명령어로 간단히 사용 가능.

✅ 실무 아키텍처 설계 체크리스트

📈 성능 & 확장성

☐ 읽기:쓰기 비율 파악 및 전략 수립
☐ DB Read Replica 설정
☐ Redis 캐시 계층 적용
☐ CDN으로 정적 파일 배포
☐ DB 인덱스 최적화 (Explain 분석)

🛡️ 가용성 & 장애

☐ Circuit Breaker 모든 외부 호출에 적용
☐ Retry + Exponential Backoff 설정
☐ Timeout 설정 (모든 I/O 작업)
☐ Health Check 엔드포인트 구현
☐ Graceful Shutdown 처리

📊 관찰가능성

☐ 구조화된 로그 (JSON 포맷)
☐ 분산 추적 (TraceID 전파)
☐ Prometheus 메트릭 수집
☐ Grafana 대시보드 구성
☐ 에러율, 지연시간 알람 설정

🔒 보안 & 데이터

☐ Rate Limiting (서비스별 설정)
☐ 민감 데이터 암호화 (at rest/in transit)
☐ RBAC 권한 체계 설계
☐ 감사 로그 (Audit Log)
☐ 정기 보안 취약점 스캔
🎉

BackendDevGuide0013 완성!

대규모 시스템 설계와 분산 아키텍처 완전 정복 A-Z

14
핵심 챕터
70+
설계 패턴/코드
A-Z
완전 가이드

로드밸런서, 캐싱, DB 샤딩, 메시지큐, MSA, Circuit Breaker,
분산 추적, CAP 이론, 실전 설계 면접까지 — 시니어도 보는 가이드 💪

반응형