1장 사용자 수에 따른 규모 확장성
이번 장에서는 한 명의 사용자를 지원하는 시스템에서 시작하여, 최종적으로는 몇백만 사용자를 지원하는 시스템을 설계해 볼 것이다. 그 과정에서 규모 확장성과 관련된 설계 문제들을 직면하고, 이를 해결하기 위한 방법을 확인해보자.
단일 서버
처음에는 모든 컴포넌트가 단 한 대의 서버에서 실행되는 간단한 시스템을 생각해 볼 수 있다. 웹 앱, DB, 캐시 등 모든 컴포넌트가 서버 한 대에서 실행된다.

사용자 요청 처리 흐름
- 사용자는 도메인 이름을 이용하여 웹사이트에 접속
- DNS 조회 결과로 IP 주소 반환
- 해당 iP 주소로 HTTP 요청을 서버로 전달
- 요청받은 웹 서버는 HTML이나 JSON 등 응답 반환
데이터베이스
사용자가 늘면 서버를 여러대 두어야 한다. 이때 서버는 웹 트래픽 처리 용도와 데이터베이스 용도로 나눌 수 있다. 웹 계층 데이터베이스를 분리하면 그 각각을 독립적으로 확장할 수 있다.

- 웹 서버와 DB의 독립적인 확장 가능
- 부하 분산의 목적
어떤 DB를 사용?
크게 관계형 DB(RDB)와 비관계형 DB(NoSQL) 사이에서 고를 수 있다.
항목 | RDB (Relational Database) | NoSQL |
---|
데이터 모델 | 테이블(행과 열) 기반의 관계형 모델 | 문서, 키-값, 그래프, 열 기반 등 다양한 모델 지원 |
스키마 | 고정된 스키마 | 유연한 스키마 (없거나 느슨한 구조) |
트랜잭션 | ACID 트랜잭션 지원 | BASE 모델, 일부는 ACID 지원 |
확장성 | 수직 확장 (서버 성능 업그레이드) | 수평 확장 (샤딩과 복제 지원) |
쿼리 언어 | 표준 SQL | 데이터베이스별 고유 쿼리 언어나 API 사용 |
성능 | 복잡한 관계형 쿼리에서 뛰어남 | 단순 읽기/쓰기 작업에서 빠름 |
유형 | 정형 데이터 처리에 적합 | 비정형, 대규모 데이터 처리에 적합 |
사용 사례 | 금융, 은행, ERP, 재고 관리 등 무결성이 중요한 시스템 | 소셜 미디어, IoT, 실시간 분석, 콘텐츠 관리 시스템 |
장점 | - 데이터 무결성 보장 - 표준화된 SQL - 관계 모델링 용이 | - 스키마 유연성 - 대규모 확장성 - 다양한 데이터 구조 |
단점 | - 스키마 변경 어려움 - 확장성 제한 | - 관계형 데이터 처리 비효율 - 표준 쿼리 언어 없음 |
일반적인 상황에서는 RDB를 사용하는 것이 최선이다. 단, 아래와 같은 경우 NoSQL DB가 바람직한 선택일 수 있다.
- 아주 낮은 응답 지연시간이 요구됨
- 다루는 데이터가 비정형이라 관계형 데이터가 아님
- 데이터를 직렬화하거나 역직렬화할 수 있기만 하면 됨
- 아주 많은 양의 데이터를 저장할 필요가 있음
수직적 규모 확장 vs 수평적 규모 확장
웹 계층과 DB 계층의 확장을 어떻게 할 수 있는지 알아보기 전에, 수직적 규모 확장과 수평적 규모 확장에 대해 알아보자.
- 수직적 규모 확장(scale up): 서버의 자원을 업그레이드 하거나 자원을 추가 (더 좋은 CPU, 더 많은 RAM 등)
- 확장의 한계가 존재, 한 서버의 자원을 무한히 늘릴 순 없음
- 장애에 대한 자동 복구 방안이나 다중화 방안이 없음
- 수평적 규모 확장(scale out): 서버를 추가하여 성능 개선 (대규모 시스템에 적합)
- 장애에 대한 자동 복구 및 다중화 가능
로드밸런서
로드밸런서는 분산 대상(서버)에 대해 트래픽 부하를 고르게 분산하는 역할을 한다. 다음은 로드밸런서가 동작하는 방식이다.

- 사용자는 로드밸런서의 public IP를 통해 접속 (웹 서버가 클라이언트의 접속을 직접 처리하지 않음)
- AWS에서는 로드밸런서를 VPC 안에 위치시키고, 로드밸런서에 인터넷 게이트웨이를 연결시키는 식으로 구성
- 보안을 위해, 서버 간 통신에는 private IP 이용 (같은 네트워크에서만 통신 가능)
- 자동 복구 가능: 서버 1 다운 시 모든 트래픽이 서버 2로 전송되어 서버 전체가 다운되는 일을 방지 가능
- 다중화: 웹 서버 계층에 서버를 추가하기만 하면 로드밸런서가 자동으로 트래픽 분산
DB 다중화
DB 다중화는 보통 master-slave 관계를 설정하는 방식으로 한다. 데이터 원본은 master DB에, 사본은 slave DB에 저장하는 방식이다. 보통은 읽기 연산이 많아 slave DB 수가 master DB보다 많다.

- master DB: 원본 저장, 쓰기&읽기 지원 (보통 쓰기만)
- slave DB: 사본 저장, 읽기만 지원
DB 다중화 시 이점
- 더 나은 성능: 읽기 연산이 slave DB에 병렬적으로 처리될 수 있어 성능이 좋아짐
- 안정성: 일부 DB 장애 시에도 데이터 보존 가능 (여러 지역에 DB 배치)
- 가용성: 데이터를 여러 DB에 걸쳐 복제하면, 장애가 발생해도 다른 서버에 있는 데이터를 가져와 사용하여 서비스를 지속할 수 있음
- 장애 시 master/slave DB간 전환으로 대응 가능
웹 + DB 다중화를 고려한 설계안

- 사용자는 DNS로부터 로드밸런서의 public IP 주소 받음
- 사용자는 해당 IP 주소를 사용해 로드밸런서에 요청
- HTTP 요청은 서버 1이나 서버 2로 전송
- 웹 서버는 읽기 요청을 slave DB로 전달, 쓰기 요청을 master DB로 전달
캐시
캐시는 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고, 뒤이은 요청이 빨리 처리될 수 있도록 하는 저장소이다. 캐시를 통해 무거운 DB 호출 작업을 줄여줘 애플리케이션 성능을 개선할 수 있다.
캐시 전략에는 여러가지가 있는데, 대표적으로 캐시 우선 읽기 전략이 있으며 그 과정은 다음과 같다.
- 웹 서버에 데이터 요청을 했을 때
- 캐시에 데이터가 있는 경우 → 즉시 데이터 반환
- 캐시에 데이터가 없는 경우 → DB에서 해당 데이터를 읽고 캐시에 씀 → 데이터 반환
캐시 사용 방식
- 보통 캐시 서버는 API를 통해 간편하게 사용할 수 있다.
SECONDS=1
cache.set('myKey', 'hi there', 3600 * SECONDS)
cache.get('myKey')
캐시 사용 시 유의점
- 캐시는 갱신은 자주 일어나지 않지만 참조는 자주 일어난다면 고려해볼만 하다.
- 캐시는 휘발성 메모리에 두기 때문에, 영속적으로 보관할 데이터를 캐시에 두는 것은 좋지 않다.
- 만료된 데이터는 캐시에서 삭제되어야 하고, 적절한 만료 정책이 필요하다.
- 만료 기한이 너무 짧으면 데이터베이스를 너무 자주 읽음
- 만료 기한이 너무 길면 원본과 차이가 날 가능성이 높아짐
- 데이터 저장소의 원본과 캐시 내의 사본 일관성을 확인해야 한다.
- 캐시 서버를 한 대만 두는 경우 해당 서버는 단일 장애 지점(SPOF)이 되어 버릴 수 있다.
- 캐시 메모리 크기는 너무 크지도 작지도 않게 적절하게 잡아야 한다.
- 너무 작으면 캐시가 자주 밀려나서 성능이 떨어짐 → overprovisioning으로 해결 가능
- LRU, LFU, FIFO 같은 캐시 방출 정책들을 상황에 맞게 적절히 사용해야 한다.
콘텐츠 전송 네트워크 (CDN)
CDN: 주로 정적 컨텐츠 전송에 쓰이는, 지리적으로 분산된 서버의 네트워크
- request path, query string, cookie, request header 등의 정보에 기반하여 정적 페이지 캐싱
- 사용자 요청 시, 그 사용자에게 가장 가까운 CDN 서버가 정적 콘텐츠 전달

- 사용자 A가 이미지 URL을 이용해 image.png에 접근
- CDN 서버의 캐시에 해당 이미지가 없는 경우, 서버는 원본 서버에 요청하여 파일을 가져옴
- 원본 서버가 파일을 CDN 서버에 반환
- CDN 서버는 파일을 캐시하고 사용자 A에게 반환
- 사용자 B가 같은 이미지에 대한 요청을 CDN 서버에 전송
- 만료되지 않은 이미지에 대한 요청은 캐시를 통해 처리
CDN 사용 시 고려사항
- 비용: 데이터 전송 양에 따라 요금 부과되므로, 자주 사용되지 않는 컨텐츠는 빼는 것이 유리
- 적절한 만료 시한 설정: time-sensitive 한 컨텐츠의 경우 적절히 만료 시한 결정
- CDN 장애에 대한 대처 방안: CDN 장애 시 원본 서버로 직접 가져오는 것 고려
- 컨텐츠 무효화: 만료되지 않은 컨텐츠도 특정 API를 사용하거나 버저닝을 통해 무효화 가능
CDN + 캐시가 추가된 설계안

- 정적 컨텐츠는 CDN을 통해 보다 나은 성능으로 제공
- 캐시를 통해 DB 부하를 줄일 수 있고, 더 빠른 응답 제공
무상태(stateless) 웹 계층
웹 계층의 수평적 확장을 위해선 상태정보(ex. 사용자 세션 데이터 등)를 웹 계층에서 제거해야 한다. 적절한 전략으로는 상태정보를 DB에 보관하고 필요할 때 가져오는 것이며, 이렇게 구성된 웹 계층을 무상태(stateless) 웹 계층이라고 한다.
상태 정보 의존적인 아키텍처
사용자 A,B,C의 세션 정보는 각각 서버 1,2,3에 저장된다고 하자.

- 만약 사용자A의 요청이 서버2로 전송되면, 세션정보가 없어 인증 실패
- 로드밸런서에서 동일한 클라이언트가 항상 같은 서버로 전송되게끔 고정 세션 기능을 제공하지만, 이는 로드밸런서에 부담을 줌
- 서버 추가나 서버 간 장애 처리가 복잡해짐
무상태 아키텍처
상태 정보를 웹 계층에서 분리하면, 어떤 서버든 상태정보를 DB에서 가져와 인증을 처리하면 된다. 또한 각 영역별 규모 확장에도 유리하다.

무상태 아키텍처를 적용하여 수평적 확장이 가능해진 설계안

- 세션 정보를 웹 계층에서 분리하여 오토스케일링이 가능해진 구조
- 세션 정보 저장소는 임의로 NoSQL로 지정 (규모 확장에 용이)
데이터 센터
가용성을 높이고 전 세계 어디서도 쾌적하게 사용할 수 있으려면, 여러 데이터 센터를 지원하는 것이 필수다. 장애가 없는 상황에서 사용자는 가장 가까운 데이터 센터로 안내되는데, 통상 이 절차를 지리적 라우팅이라고 부른다.
지리적 라우팅

- 적절한 비율로 지리적 라우팅이 된다고 가정 (60:40)
- 만약 이때 US-West 데이터 센터에 심각한 장애가 발생하면, 모든 트래픽은 장애가 없는 US-East 전달 (100:0)
- 다중 데이터 센터 구축 시 고려사항
- 트래픽 우회: 올바른 데이터 센터로 트래픽을 보내는 효과적인 방법 필요 (GeoDNS 등)
- 데이터 동기화: 데이터 센터마다 별도의 DB 사용 시, 데이터 동기화(데이터 센터 다중화) 필요
- 테스트와 배포: 여러 데이터 센터를 사용하는 시스템이라면, 애플리케이션을 여러 위치에서 테스트하는 것이 좋음
메시지 큐
시스템 확장을 위해서는 시스템의 컴포넌트들을 분리하여 각기 독립적으로 확장시키는 것이 중요하다. 컴포넌트 분리 및 확장을 위해 채용하고 있는 핵심 전략으로는 메세지 큐 도입이 있다.
메세지 큐를 도입하면 컴포넌트 혹은 서버간의 느슨한 결합을 가능하게 한다. 메시지 큐가 가지는 무손실, 비동기 통신의 특성으로 컴포넌트간 메시지 버퍼 역할을 하기 때문이다. 이는 곧 안정적인 규모 확장으로 이어진다.

로그, 메트릭 그리고 자동화
시스템 규모가 커질수록 로그나 메트릭, 그리고 자동화를 필수적으로 고려해야 한다.

- 로그: 에러 모니터링은 중요, 로그를 단일 서비스로 모아주는 도구를 사용할 수도 있음
- 메트릭: 비즈니스와 관련된 지표 등 다양한 인사이트 획득 가능, 시스템 상태 파악에 용이
- 호스팅 단위 메트릭(시스템): CPU, 메모리, 디스크IO 등
- Aggregated 메트릭(시스템): DB 성능, 캐시 성능 등
- 핵심 비즈니스 메트릭(비즈니스): daily active user, 수익, retention 정보 등
- 자동화: 자동화 도구를 통해 CI/CD(코드 검증, 빌드, 테스트, 배포 절차) 자동화 가능 → 생산성, 안정성 향상
- CI/CD 파이프라인 구축: Jenkins, GitLab CI, GitHub Actions
- 인프라 자동화: Terraform, Ansible
- 모니터링 및 알림 자동화: Prometheus, Grafana
데이터베이스의 규모 확장
DB도 수직적, 그리고 수평적 규모 확장을 할 수 있다. 서버와 마찬가지로 수직적 규모 확장은 한계(자원, SPOF, 비용 등)가 있어 수평적 확장을 고려해야 한다.
수평적 확장 (샤딩)
샤딩은 DB 서버를 샤드(Shard)라는 작은 단위로 분할하는 것이다. 모든 샤드는 같은 스키마를 쓰지만, 샤드에 보관되는 데이터 사이에는 중복이 없다.

- 예를 들어, user_id % 4를 해시 함수로 사용하여 데이터가 보관되는 샤드를 정할 수 있다.
샤딩 전략 구현 시 고려사항
샤딩을 도입할 때 가장 중요한 것은 샤딩 키 (혹은 파티션 키)를 어떻게 정하느냐 하는 것이다. 특히 데이터를 고르게 분할 할 수 있도록 적절한 키를 선정하는 것이 중요하다. 다음은 샤딩 도입 시 고려해야 할 것들이다.
- 데이터의 재 샤딩
- 데이터가 너무 많아져서 하나의 샤드로는 더 이상 감당하기 어렵거나, 샤드 간 데이터 분포가 균등하지 못하여 어떤 샤드에 할당된 공간 소모가 다른 샤드에 비해 빨리 진행될 때 필요
- 샤드 키를 계산하는 함수를 변경하고 데이터 재배치 필요
- 유명 인사 문제
- 핫스팟 키 문제라고도 부르는데, 특정 샤드에 쿼리가 집중되어 서버에 과부하가 걸리는 문제
- 실제로 유명인에 대해서, 혹은 특정 지역에 대해 트래픽이 몰리는 사례가 있나..?
- 조인과 비정규화
- 일단 하나의 데이터베이스를 여러 샤드 서버로 쪼개고 나면, 여러 샤드에 걸친 데이터를 조인하기가 어려움
- DB 비정규화를 고려, 하나의 테이블에서 쿼리가 수행될 수 있도록 하는 것
샤딩을 적용한 설계안
