[가면사배 시리즈 #1] 사용자 수에 따른 규모 확장성 - 단일 서버에서 수백만 사용자까지

2025. 9. 3. 20:00·📈 Career & Growth/🎓 Learning Journey
반응형

시작하며

항해 플러스 동기들과 함께 "가상 면접 사례로 배우는 대규모 시스템 설계 기초(가면사배)" 독서 스터디를 시작했습니다!

앞으로 각 장마다 학습한 내용을 정리해서 공유할 예정인데, 첫 번째로 1장 "사용자 수에 따른 규모 확장성"을 읽고 나니 정말 많은 걸 배웠더라고요.

단일 서버에서 수백만 사용자를 지원하는 시스템까지의 진화 과정을 단계별로 설명해놓은 게 인상적이었습니다. 개발자로서 항상 "우리 서비스가 사용자가 많아지면 어떻게 대응해야 할까?"라는 고민이 있었는데, 이 책에서 체계적으로 정리해놓은 내용이 정말 도움이 됐어요.

독서 스터디 동기들과 함께 토론하면서 더 깊이 이해할 수 있었고, 혼자만 알고 있기 아까워서 핵심 내용들을 정리해서 공유해보려고 합니다.

책에서 제시하는 11단계 시스템 진화 과정

1단계: 단일 서버 - 모든 것의 시작

책에서는 가장 간단한 형태부터 시작합니다. 하나의 물리적 서버 안에 모든 컴포넌트가 함께 실행되는 구조입니다.

graph TD
    A[사용자] -->|1. DNS 조회| B[DNS 서버]
    B -->|2. IP 주소 반환| A
    A -->|3. HTTP 요청| C[단일 서버 - 15.125.23.214]

    subgraph "단일 서버 내부"
        D[웹 애플리케이션<br/>- 비즈니스 로직<br/>- API 처리]
        E[데이터베이스<br/>- 사용자 데이터<br/>- 애플리케이션 데이터]
        F[캐시<br/>- 메모리 캐시<br/>- 세션 데이터]

        D <--> E
        D <--> F
    end

    C --> D
    C -->|4. HTML/JSON 응답| A

실제 구현 예시:

// 모든 것이 하나의 서버에서 실행
const express = require("express");
const app = express();

// 메모리 캐시 (단순한 Map 객체)
const cache = new Map();

// 데이터베이스 연결 (같은 서버)
const db = require("./database"); // localhost 연결

app.get("/api/users/:id", async (req, res) => {
  // 캐시 확인
  if (cache.has(req.params.id)) {
    return res.json(cache.get(req.params.id));
  }

  // DB 조회
  const user = await db.query("SELECT * FROM users WHERE id = ?", [
    req.params.id,
  ]);
  cache.set(req.params.id, user);

  res.json(user);
});

app.listen(3000); // 모든 것이 포트 3000에서 실행

장점:

  • 구현이 간단하고 비용이 저렴
  • 소규모 트래픽에는 충분
  • 개발과 배포가 단순함
  • 네트워크 지연이 없음

한계:

  • 단일 장애 지점(SPOF) 문제
  • 확장성 제한 (CPU, 메모리, 디스크 한계)
  • 성능 병목 (모든 요청이 하나의 서버로)
  • 컴포넌트 간 리소스 경합

이 부분을 읽으면서 "아, 우리도 처음엔 이랬지"라는 생각이 들었습니다. 모든 서비스가 이런 단순한 구조에서 시작하는 게 자연스러운 것 같아요. 실제로 스타트업이나 개인 프로젝트에서는 이런 구조로 시작해서 점진적으로 확장해나가는 게 일반적이죠.

2단계: 데이터베이스 분리 - 첫 번째 분리

다음 단계는 웹 서버와 데이터베이스를 분리하는 것입니다. 각각 독립적으로 확장할 수 있게 되죠.

graph TD
    A[사용자] -->|HTTP 요청| B[웹 서버]
    B -->|SQL 쿼리| C[데이터베이스 서버]
    C -->|데이터 반환| B
    B -->|HTML/JSON 응답| A

    style B fill:#e1f5fe
    style C fill:#f3e5f5

책에서 흥미로웠던 부분은 데이터베이스 선택 기준을 표로 정리해놓은 것이었습니다:

관계형 데이터베이스 비관계형 데이터베이스
복잡한 쿼리와 트랜잭션 낮은 응답 지연시간
ACID 속성 필요 비정형 데이터
성숙한 생태계 대용량 데이터

실무에서 "언제 NoSQL을 써야 할까?"라고 고민했었는데, 이런 기준이 명확하게 정리되어 있어서 좋았습니다.

3단계: 로드밸런서 도입 - 가용성의 시작

로드밸런서를 도입해서 여러 웹 서버로 트래픽을 분산시키는 단계입니다.

graph TD
    A[사용자] -->|HTTP 요청| B[로드밸런서<br/>공개 IP]
    B -->|트래픽 분산| C[웹 서버 1<br/>사설 IP]
    B -->|트래픽 분산| D[웹 서버 2<br/>사설 IP]
    C -->|SQL 쿼리| E[데이터베이스]
    D -->|SQL 쿼리| E
    E -->|데이터 반환| C
    E -->|데이터 반환| D
    C -->|응답| B
    D -->|응답| B
    B -->|HTML/JSON 응답| A

    style B fill:#ffeb3b
    style C fill:#e1f5fe
    style D fill:#e1f5fe
    style E fill:#f3e5f5

핵심 개념:

  • 공개 IP는 로드밸런서만 보유
  • 웹 서버들은 사설 IP로 통신
  • 장애 시 자동 복구 가능

책에서 강조한 건 "가용성 향상"이었습니다. 한 서버가 죽어도 서비스가 계속 돌아간다는 것, 정말 중요한 개념이죠.

4단계: 데이터베이스 다중화 - 읽기 성능 개선

주(Master) 데이터베이스와 부(Slave) 데이터베이스로 나누는 단계입니다.

graph TD
    A[사용자] -->|HTTP 요청| B[로드밸런서]
    B --> C[웹 서버 1]
    B --> D[웹 서버 2]

    C -->|쓰기 연산| E[주 데이터베이스<br/>Master]
    D -->|쓰기 연산| E
    C -->|읽기 연산| F[부 데이터베이스<br/>Slave 1]
    C -->|읽기 연산| G[부 데이터베이스<br/>Slave 2]
    D -->|읽기 연산| F
    D -->|읽기 연산| G

    E -.->|데이터 복제| F
    E -.->|데이터 복제| G

    style E fill:#ff5722,color:#fff
    style F fill:#4caf50,color:#fff
    style G fill:#4caf50,color:#fff

구조:

  • 주 데이터베이스: 쓰기 연산
  • 부 데이터베이스: 읽기 연산
  • 데이터 복제를 통한 동기화

장애 시나리오도 자세히 설명되어 있었습니다:

  • 부 서버 장애: 읽기 트래픽을 주 서버나 다른 부 서버로 전환
  • 주 서버 장애: 부 서버를 주 서버로 승격, 복구 스크립트 실행

이 부분에서 "아, 실제 운영에서는 이런 시나리오들을 다 고려해야 하는구나"라는 걸 깨달았습니다.

5단계: 캐시 계층 - 성능의 핵심

캐시 부분이 개인적으로 가장 흥미로웠습니다. 책에서 제시한 캐시 우선 읽기(Cache-Aside) 패턴:

sequenceDiagram
    participant U as 사용자
    participant W as 웹 서버
    participant C as 캐시
    participant D as 데이터베이스

    U->>W: 사용자 데이터 요청
    W->>C: 캐시에서 데이터 확인
    alt 캐시 히트
        C->>W: 캐시된 데이터 반환
        W->>U: 응답 (빠름)
    else 캐시 미스
        C->>W: 데이터 없음
        W->>D: 데이터베이스 조회
        D->>W: 데이터 반환
        W->>C: 캐시에 데이터 저장
        W->>U: 응답 (느림)
    end
async function getUser(userId) {
  // 1. 캐시에서 확인
  const user = await cache.get(`user:${userId}`);
  if (user) {
    return JSON.parse(user);
  }

  // 2. 캐시 미스 시 DB 조회
  const userData = await database.query("SELECT * FROM users WHERE id = ?", [
    userId,
  ]);

  // 3. 캐시에 저장 (1시간 TTL)
  await cache.setex(`user:${userId}`, 3600, JSON.stringify(userData));
  return userData;
}

고려사항들도 정말 실무적이었습니다:

  • 캐시 적용 시점: 읽기가 빈번하고 쓰기가 적은 데이터
  • 데이터 일관성: 원본과 캐시 간 동기화
  • 만료 정책: TTL 설정
  • 장애 대응: 캐시 서버 다중화
  • 데이터 방출 정책: LRU, LFU, FIFO

이런 세부사항들까지 고려해야 한다는 걸 보고 "캐시가 단순해 보이지만 실제로는 복잡하구나"라고 생각했습니다.

6단계: CDN - 정적 콘텐츠 최적화

CDN(콘텐츠 전송 네트워크) 도입 단계입니다.

graph TD
    A[사용자 - 서울] -->|정적 콘텐츠 요청| B[CDN 서울]
    C[사용자 - 도쿄] -->|정적 콘텐츠 요청| D[CDN 도쿄]
    E[사용자 - 뉴욕] -->|정적 콘텐츠 요청| F[CDN 뉴욕]

    B -.->|캐시 미스 시| G[원본 서버]
    D -.->|캐시 미스 시| G
    F -.->|캐시 미스 시| G

    G -.->|콘텐츠 전송| B
    G -.->|콘텐츠 전송| D
    G -.->|콘텐츠 전송| F

    style B fill:#2196f3,color:#fff
    style D fill:#2196f3,color:#fff
    style F fill:#2196f3,color:#fff
    style G fill:#ff9800,color:#fff

동작 과정:

  1. 사용자가 정적 콘텐츠 요청
  2. CDN에 캐시된 콘텐츠가 있으면 반환
  3. 없으면 원본 서버에서 가져와 캐시 후 반환

고려사항:

  • 비용: 데이터 전송량에 따른 과금
  • 적절한 TTL 설정
  • CDN 장애 대응 방안
  • 콘텐츠 무효화 방법

특히 "비용" 부분이 현실적이었습니다. 자주 사용되지 않는 콘텐츠를 캐싱하는 건 이득이 크지 않다는 점, 실무에서 정말 중요한 고려사항이죠.

7단계: 무상태 웹 계층 - 확장성의 핵심

개인적으로 가장 중요하다고 생각한 부분입니다.

상태 정보 의존적 vs 무상태 비교:

상태 정보 의존적 무상태
서버가 클라이언트 상태 보관 상태 정보를 외부 저장소에 보관
특정 서버로만 요청 전달 필요 어떤 서버로든 요청 전달 가능
확장성 제한 자동 확장 가능

구현 방법:

  • 세션 데이터를 Redis, Memcached 등 외부 저장소로 이동
  • 데이터베이스에 사용자 상태 저장
  • NoSQL 활용

이 개념을 이해하고 나니 "왜 세션을 외부 저장소에 두라고 하는지" 명확해졌습니다.

8단계: 데이터 센터 - 글로벌 서비스

지리적 라우팅(GeoDNS)을 통해 사용자를 가장 가까운 데이터 센터로 라우팅하는 단계입니다.

기술적 과제:

  • 트래픽 우회: GeoDNS를 통한 효과적인 라우팅
  • 데이터 동기화: 데이터 센터 간 데이터 일관성
  • 테스트와 배포: 여러 지역에서의 테스트 및 배포 자동화

글로벌 서비스를 운영할 때 고려해야 할 점들이 정말 많다는 걸 깨달았습니다.

9단계: 메시지 큐 - 비동기 처리

graph LR
    A[웹 서버<br/>생산자] -->|메시지 발행| B[메시지 큐]
    B -->|메시지 소비| C[이미지 처리<br/>소비자 1]
    B -->|메시지 소비| D[이메일 발송<br/>소비자 2]
    B -->|메시지 소비| E[로그 분석<br/>소비자 3]

    F[사용자] -->|이미지 업로드| A
    A -->|즉시 응답| F

    style A fill:#4caf50,color:#fff
    style B fill:#ff9800,color:#fff
    style C fill:#2196f3,color:#fff
    style D fill:#2196f3,color:#fff
    style E fill:#2196f3,color:#fff

구성 요소:

  • 생산자(Producer): 메시지 발행
  • 메시지 큐: 메시지 버퍼 역할
  • 소비자(Consumer): 메시지 처리

장점:

  • 느슨한 결합: 생산자와 소비자 독립적 운영
  • 안정성: 메시지 지속성 보장
  • 확장성: 각 컴포넌트 독립적 확장

사용 사례도 구체적이었습니다:

  • 이미지 처리: 업로드 → 큐 → 비동기 처리
  • 이메일 발송: 요청 → 큐 → 배치 처리
  • 로그 처리: 수집 → 큐 → 분석

10단계: 로그, 메트릭, 자동화 - 운영의 기초

로그: 에러 로그 모니터링, 중앙화된 로그 수집 시스템
메트릭: 호스트 단위, 종합 메트릭, 비즈니스 메트릭
자동화: 지속적 통합(CI), 자동 빌드, 테스트, 배포

이 부분에서 "시스템을 만드는 것과 운영하는 것은 다른 영역이구나"라는 걸 느꼈습니다.

11단계: 데이터베이스 샤딩 - 최종 확장

graph TD
    A[사용자 요청<br/>user_id: 1001] -->|해시 함수| B[샤딩 로직<br/>1001 % 4 = 1]

    B --> C[샤드 0<br/>user_id % 4 = 0]
    B --> D[샤드 1<br/>user_id % 4 = 1]
    B --> E[샤드 2<br/>user_id % 4 = 2]
    B --> F[샤드 3<br/>user_id % 4 = 3]

    D -->|데이터 조회| G[사용자 1001 데이터]

    style D fill:#4caf50,color:#fff
    style C fill:#e0e0e0
    style E fill:#e0e0e0
    style F fill:#e0e0e0
    style G fill:#ffeb3b

샤딩 전략: 샤딩 키 선택, 해시 기반 샤딩

고려사항:

  • 데이터 재샤딩: 샤드 추가 시 데이터 재분배
  • 유명인사 문제: 특정 샤드에 트래픽 집중
  • 조인과 비정규화: 여러 샤드 간 조인의 어려움

샤딩이 가장 복잡한 단계라는 걸 알 수 있었습니다.

책에서 배운 핵심 아키텍처 패턴들

수직적 확장 vs 수평적 확장

수직적 확장 (Scale Up) 수평적 확장 (Scale Out)
서버 성능 향상 서버 수량 증가
구현 단순 복잡성 증가
하드웨어 한계 존재 이론적 무제한 확장
SPOF 위험 장애 내성
고비용 상대적 저비용

로드밸런싱 전략

  • 라운드 로빈: 순차적으로 서버 선택
  • 가중 라운드 로빈: 서버 성능에 따른 가중치 적용
  • 최소 연결: 연결 수가 가장 적은 서버 선택
  • IP 해시: 클라이언트 IP 기반 서버 선택

캐시 패턴

  • Cache-Aside: 애플리케이션이 캐시 관리
  • Write-Through: 쓰기 시 캐시와 DB 동시 업데이트
  • Write-Behind: 캐시 먼저 쓰고 나중에 DB 업데이트
  • Refresh-Ahead: 만료 전 미리 캐시 갱신

트레이드오프에 대한 깊은 이해

책에서 가장 인상 깊었던 부분은 트레이드오프 분석이었습니다.

성능 vs 일관성

  • 강한 일관성: 모든 노드에서 동일한 데이터, 성능 저하
  • 최종 일관성: 일시적 불일치 허용, 성능 향상

CAP 정리

  • 일관성(Consistency): 모든 노드가 동일한 데이터
  • 가용성(Availability): 시스템이 항상 응답
  • 분할 내성(Partition Tolerance): 네트워크 분할 시에도 동작

비용 vs 성능

  • 고성능 하드웨어: 높은 비용, 뛰어난 성능
  • 분산 시스템: 상대적 저비용, 복잡성 증가

"모든 기술 선택에는 트레이드오프가 있다"는 걸 명확하게 이해하게 됐습니다.

실무에 적용할 수 있는 인사이트들

1. 점진적 확장의 중요성

  • 처음부터 복잡한 아키텍처 구축보다는 필요에 따른 점진적 확장
  • 각 단계에서 병목 지점을 파악하고 해결
  • 과도한 엔지니어링(Over-engineering) 방지

2. 모니터링의 필수성

  • 시스템 확장 전 현재 상태 정확한 파악 필요
  • 메트릭 기반 의사결정
  • 장애 예방을 위한 사전 모니터링

3. 데이터 중심 설계

  • 데이터의 특성(읽기/쓰기 비율, 크기, 접근 패턴)에 따른 저장소 선택
  • 데이터 일관성 요구사항 분석
  • 백업 및 복구 전략 수립

4. 장애 대응 설계

  • 단일 장애 지점 제거
  • 장애 시 자동 복구 메커니즘
  • 우아한 성능 저하(Graceful Degradation)

책의 핵심 메시지

"수백만 사용자를 지원하는 시스템을 설계하는 것은 도전적인 과제이며, 지속적인 계량과 끝없는 개선이 요구되는 여정이다."

이 문장이 책의 핵심을 잘 표현한다고 생각합니다.

배운 핵심 원칙들:

  1. 점진적 확장: 필요에 따른 단계적 시스템 진화
  2. 병목 지점 파악: 각 단계에서의 성능 제약 요소 식별
  3. 트레이드오프 이해: 성능, 일관성, 비용 간의 균형
  4. 장애 대응: 안정적인 서비스를 위한 다중화와 모니터링
  5. 데이터 중심 설계: 데이터 특성에 맞는 아키텍처 선택

마무리

이 책의 1장을 읽으면서 시스템 설계에 대한 체계적인 사고 과정을 배울 수 있었습니다. 특히 "단계별 진화"라는 접근 방식이 인상 깊었어요.

실무에서 "처음부터 완벽한 시스템을 만들어야 한다"는 압박감을 느꼈었는데, 이 책을 통해 "필요에 따라 점진적으로 개선해나가면 된다"는 걸 깨달았습니다.

다음 포스트에서는 2장 "개략적인 규모 추정"을 다룰 예정입니다. 시스템 설계할 때 용량 계산하는 방법을 다룬다고 하는데, 이것도 정말 필요한 내용일 것 같아요.

이 내용으로 토론하면 좋을 질문들을 정리해봤습니다:

🤔 시스템 설계 접근법에 대한 질문

  • 단일 서버에서 시작하는 것 vs 처음부터 분산 시스템으로 설계하는 것, 어떤 상황에서 어떤 선택을 해야 할까요?
  • 수직 확장과 수평 확장의 기준점은 어떻게 정해야 할까요?
  • 로드밸런서 도입 시점을 어떻게 판단하시나요?

💾 데이터베이스 확장에 대한 질문

  • 읽기 복제본을 언제 도입해야 할까요? 트래픽 기준이 있나요?
  • 데이터베이스 샤딩을 고려해야 하는 시점은 언제인가요?
  • NoSQL vs RDBMS 선택 기준은 무엇인가요?

⚡ 캐시 전략에 대한 질문

  • 어떤 데이터를 캐시해야 하고, 어떤 데이터는 캐시하지 말아야 할까요?
  • 캐시 무효화 전략 중 가장 효과적인 방법은 무엇인가요?
  • 메모리 캐시 vs 분산 캐시, 언제 어떤 것을 선택해야 할까요?

🏗️ 아키텍처 진화에 대한 질문

  • 마이크로서비스로 전환하는 적절한 시점은 언제일까요?
  • CDN 도입의 비용 대비 효과를 어떻게 측정하나요?
  • 메시지 큐 도입이 필요한 상황은 어떤 경우인가요?

📊 모니터링과 운영에 대한 질문

  • 시스템 확장 시점을 판단하는 핵심 지표는 무엇인가요?
  • 장애 대응을 위한 모니터링 체계는 어떻게 구축해야 할까요?
  • 성능 병목점을 찾는 가장 효과적인 방법은 무엇인가요?

댓글로 공유해주시면 함께 배워나갈 수 있을 것 같습니다!

반응형

'📈 Career & Growth > 🎓 Learning Journey' 카테고리의 다른 글

[가면사배 시리즈 #2] 개략적인 규모 추정 - 시스템 설계의 첫 번째 관문  (0) 2025.09.22
항해 플러스 백엔드 코스 6기 수료 회고  (1) 2024.12.08
항해 플러스 백엔드 코스 6기 9주차 회고 WIL  (1) 2024.11.25
항해 플러스 백엔드 코스 6기 8주차 회고 WIL  (0) 2024.11.17
항해 플러스 백엔드 코스 6기 7주차 회고 WIL  (1) 2024.11.10
'📈 Career & Growth/🎓 Learning Journey' 카테고리의 다른 글
  • [가면사배 시리즈 #2] 개략적인 규모 추정 - 시스템 설계의 첫 번째 관문
  • 항해 플러스 백엔드 코스 6기 수료 회고
  • 항해 플러스 백엔드 코스 6기 9주차 회고 WIL
  • 항해 플러스 백엔드 코스 6기 8주차 회고 WIL
KilPenguin
KilPenguin
penguin-dev 님의 블로그 입니다.
    반응형
  • KilPenguin
    Penguin Dev
    KilPenguin
  • 전체
    오늘
    어제
    • 분류 전체보기 (41)
      • 🏗️ Architecture & Design (2)
        • 📐 Clean Architecture (2)
        • 🔄 Design Patterns (0)
      • ⚡ Performance & Optimizatio.. (4)
        • 🗄️ Database Tuning (2)
        • 🚀 Caching Strategy (1)
        • 🖥️ Server Optimization (1)
      • 💻 Backend Development (9)
        • 🔒 Concurrency Control (5)
        • 🌱 Spring Framework (3)
        • 📨 Event-Driven Architecture (0)
        • ☕ Java Fundamentals (1)
      • 🔧 Dev Tools & Environment (4)
        • 🔄 Version Control (2)
        • 📝 Documentation Tools (1)
        • 🎨 Blog Setup (1)
      • 📈 Career & Growth (21)
        • 🎓 Learning Journey (15)
        • 🎤 Conference & Community (6)
      • 🎯 Personal (1)
        • 👋 Introduction (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    항해플러스
    항해플러스후기
    인프런
    항해99
    판교퇴근길밋업
    항해플러스백엔드
    개발바닥밋업
    항해솔직후기
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
KilPenguin
[가면사배 시리즈 #1] 사용자 수에 따른 규모 확장성 - 단일 서버에서 수백만 사용자까지
상단으로

티스토리툴바