Command Palette

Search for a command to run...

이 글의 핵심은 서버 1대의 단순한 구조가 병목을 만나며 여러 계층으로 나뉘는 확장 순서입니다.

대규모 시스템 설계는 처음부터 거대한 구조를 외우는 공부가 아닙니다. 작은 서비스가 사용자를 더 많이 받게 되면서 어떤 병목을 만나고, 그 병목을 어떤 컴포넌트로 분리하는지 확인하는 공부에 가깝습니다.

1장은 서버 1대에서 시작한 서비스를 수백만 사용자 규모까지 키우는 흐름을 다룹니다. 웹 서버와 데이터베이스를 분리하고, 로드밸런서와 캐시를 넣고, 마지막에는 메시지 큐와 샤딩까지 이어집니다.

이 글은 처음 시스템 설계를 공부하는 독자가 각 용어에서 멈추지 않도록 핵심 단어를 먼저 풀어 쓰고, 각 선택이 왜 필요한지 정리하는 방식으로 작성했습니다.

확장 흐름은 일곱 가지 용어에서 시작합니다

용어
서버사용자의 요청을 받아 처리하는 컴퓨터 또는 프로그램입니다. 웹 페이지를 보여주거나 API 응답을 만드는 역할을 합니다.
데이터베이스사용자 정보, 게시글, 주문 내역처럼 오래 보관해야 하는 데이터를 저장하는 시스템입니다.
병목전체 성능을 막는 가장 느린 지점입니다. 웹 서버가 빠르더라도 데이터베이스가 느리면 전체 서비스가 느려집니다.
수직 확장서버 1대의 성능을 더 좋은 장비로 올리는 방식입니다. CPU, 메모리, 디스크를 키웁니다.
수평 확장서버 대수를 늘리는 방식입니다. 같은 역할을 하는 서버를 여러 대 두고 요청을 나눕니다.
SPOFSingle Point of Failure의 약자입니다. 한 곳이 죽으면 전체가 멈추는 단일 장애점을 뜻합니다.
가용성서비스가 정상적으로 동작하는 시간의 비율입니다. 장애가 나도 계속 서비스를 제공할 수 있는 능력과 연결됩니다.

첫 구조는 단순하지만 오래 버티기 어렵습니다

처음에는 서버 1대만으로도 서비스를 만들 수 있습니다.

사용자 -> 서버 1대 -> 데이터베이스 1개

이 구조에서는 웹 요청 처리와 데이터 저장이 한곳에 모입니다. 사용자가 적으면 문제가 잘 보이지 않습니다. 하지만 요청이 늘면 서버가 처리할 일이 빠르게 많아집니다.

웹 요청 처리는 CPU와 메모리를 많이 씁니다. 데이터베이스는 디스크 입출력과 인덱스 관리가 중요합니다. 두 작업이 같은 장비에서 경쟁하면 한쪽 병목이 다른 계층까지 끌고 갑니다.

그래서 첫 번째 분리는 웹 계층과 데이터 계층을 나누는 일입니다.

사용자 -> 웹 서버 -> 데이터베이스 서버

웹 서버는 요청을 받고 응답을 만듭니다. 데이터베이스 서버는 데이터를 저장하고 조회합니다. 두 역할을 나누면 CPU, 메모리, 디스크 입출력 병목을 따로 관찰하고 확장할 수 있습니다.

서버 성능을 키울지, 서버 대수를 늘릴지 선택해야 합니다

사용자가 더 늘면 웹 서버 한 대가 다시 한계에 도달합니다. 이때 선택지는 크게 두 가지입니다.

방식설명장점한계
수직 확장더 좋은 서버 1대로 바꿉니다.구조가 단순하고 코드 변경이 적습니다.장비 가격이 급격히 비싸지고, 결국 한 대라는 한계가 남습니다.
수평 확장같은 서버를 여러 대 둡니다.서버를 추가하며 처리량을 늘릴 수 있습니다.요청을 나누는 장치와 무상태 구조가 필요합니다.

대규모 시스템은 보통 수평 확장을 기본 방향으로 잡습니다. 서버 한 대를 계속 키우는 방식은 어느 순간 비용과 물리적 한계에 막히기 때문입니다.

수평 확장을 하려면 앞단에 로드밸런서가 필요합니다.

사용자 -> 로드밸런서 -> 웹 서버 1
                    -> 웹 서버 2
                    -> 웹 서버 3

로드밸런서는 사용자의 요청을 여러 서버에 나누어 보내는 장치입니다. 특정 서버가 장애를 내면 그 서버로 요청을 보내지 않고 남은 서버로 우회시킵니다.

여기서 중요한 기준은 SPOF 제거입니다. 웹 서버를 여러 대로 늘려도 로드밸런서가 한 대뿐이면 로드밸런서가 새로운 단일 장애점이 됩니다. 그래서 로드밸런서도 보통 Active-Standby 구조로 둡니다.

Active-Standby는 평소에는 한 대가 처리하고, 대기 중인 다른 한 대가 장애 시 역할을 넘겨받는 방식입니다.

데이터베이스는 읽기와 쓰기를 나누며 확장합니다

웹 서버를 여러 대로 늘리면 다음 병목은 데이터베이스로 옮겨갑니다. 대부분의 서비스는 쓰기보다 읽기가 훨씬 많습니다. 게시글을 한 번 작성해도 조회는 수백 번, 수천 번 일어날 수 있습니다.

그래서 데이터베이스를 주 데이터베이스와 복제 데이터베이스로 나눕니다.

쓰기 요청 -> 주 데이터베이스
읽기 요청 -> 복제 데이터베이스 1
          -> 복제 데이터베이스 2

주 데이터베이스는 쓰기를 담당합니다. 복제 데이터베이스는 읽기를 담당합니다. 이렇게 하면 읽기 부하를 여러 서버로 나눌 수 있습니다.

여기서 복제는 주 데이터베이스의 변경 내용을 복제 데이터베이스로 전달하는 과정입니다. MySQL에서는 변경 내역을 바이너리 로그에 기록하고, 복제 데이터베이스가 그 로그를 따라가며 같은 변경을 적용합니다.

주의할 점도 있습니다. 복제가 비동기 방식이면 복제 지연이 생길 수 있습니다. 방금 수정한 데이터를 바로 읽었는데 복제 데이터베이스에는 아직 반영되지 않았을 수 있습니다.

이 문제는 Read-Your-Writes 패턴으로 다룹니다. 사용자가 방금 쓴 데이터처럼 최신성이 중요한 읽기는 주 데이터베이스에서 읽고, 일반 조회는 복제 데이터베이스에서 읽는 방식입니다.

캐시는 반복 조회를 줄이는 장치입니다

데이터베이스 앞에는 캐시를 둘 수 있습니다.

웹 서버 -> 캐시 -> 데이터베이스

캐시는 자주 쓰는 데이터를 더 빠른 저장소에 잠시 보관하는 방식입니다. Redis 같은 인메모리 저장소를 많이 사용합니다. 인메모리는 디스크보다 훨씬 빠르지만, 보통 영구 저장소의 역할을 하지는 않습니다.

예를 들어 데이터베이스 조회가 100ms 걸리고 캐시 조회가 1ms 걸린다고 가정하겠습니다. 전체 요청의 80%가 캐시에서 해결되면 데이터베이스가 처리해야 할 요청은 20%로 줄어듭니다.

캐시 전략은 데이터 특성에 따라 달라집니다.

전략동작적합한 상황
Read-Through캐시에 없으면 데이터베이스에서 읽고 캐시에 저장합니다.읽기가 많은 데이터
Write-Through쓸 때 캐시와 데이터베이스를 함께 갱신합니다.최신성이 중요한 데이터
Write-Back먼저 캐시에 쓰고 데이터베이스에는 나중에 반영합니다.빠른 응답이 중요하고 일부 지연을 허용할 수 있는 데이터

캐시를 쓸 때 가장 먼저 확인할 질문은 하나입니다.

이 데이터가 몇 초 정도 오래되어도 괜찮은가요?

괜찮다면 TTL을 둘 수 있습니다. TTL은 Time To Live의 약자이며, 캐시 데이터가 살아 있는 시간을 뜻합니다. 괜찮지 않다면 명시적으로 캐시를 지우거나 갱신하는 전략이 필요합니다.

CDN은 정적 파일을 사용자 가까이 둡니다

캐시와 비슷해 보이지만 CDN은 역할이 다릅니다.

CDN은 Content Delivery Network의 약자입니다. 이미지, CSS, JavaScript, 동영상 같은 정적 파일을 사용자와 가까운 엣지 서버에 저장해 빠르게 전달합니다.

사용자 -> 가까운 CDN 엣지 서버
      -> 원본 서버

캐시는 보통 API 응답이나 세션 같은 동적 데이터에 가깝습니다. CDN은 여러 사용자가 공유하는 정적 파일에 가깝습니다.

한 페이지를 열 때 이미지는 CDN에서 받고, 사용자별 데이터는 API 서버와 캐시를 통해 받을 수 있습니다. 역할을 나누면 웹 서버와 데이터베이스가 정적 파일 전달까지 떠안지 않아도 됩니다.

무상태 구조가 되어야 서버를 자유롭게 늘릴 수 있습니다

수평 확장을 하려면 서버가 상태를 직접 들고 있지 않아야 합니다.

상태는 세션, 로그인 정보, 장바구니처럼 요청 사이에 유지되어야 하는 정보를 뜻합니다. 이 상태를 웹 서버 메모리에 저장하면 문제가 생깁니다.

첫 요청 -> 웹 서버 1에 세션 저장
다음 요청 -> 웹 서버 2로 이동 -> 세션 없음

이 문제를 피하려면 상태를 서버 밖으로 빼야 합니다.

웹 서버 1 \
웹 서버 2  -> Redis 또는 별도 세션 저장소
웹 서버 3 /

웹 서버는 어떤 서버든 같은 세션 저장소를 조회합니다. 그러면 서버를 추가하거나 제거해도 사용자는 같은 로그인 상태를 유지할 수 있습니다.

이것이 무상태 아키텍처입니다. 서버가 아무 상태도 처리하지 않는다는 뜻은 아닙니다. 상태를 개별 서버 안에 고정하지 않는다는 뜻에 가깝습니다.

오래 걸리는 작업은 메시지 큐로 분리합니다

사용자가 요청한 작업 중에는 즉시 끝내기 어려운 일이 있습니다. 이미지 변환, 이메일 발송, 알림 전송, 대용량 파일 처리 같은 작업입니다.

이 작업을 사용자 요청 안에서 모두 처리하면 응답 시간이 길어집니다. 그래서 메시지 큐를 사용합니다.

요청 처리 서버 -> 메시지 큐 -> 작업 처리 서버

메시지 큐는 해야 할 일을 줄 세워 보관하는 시스템입니다. 앞단 서버는 작업 메시지를 큐에 넣고 빠르게 응답합니다. 뒤쪽 작업 서버가 큐에서 메시지를 꺼내 처리합니다.

여기서 생산자는 메시지를 넣는 쪽입니다. 소비자는 메시지를 꺼내 처리하는 쪽입니다. 트래픽이 늘면 소비자 수를 늘려 병렬 처리할 수 있습니다.

ack와 nack도 중요한 용어입니다. ack는 처리 완료 확인입니다. 소비자가 작업을 끝내고 ack를 보내야 큐에서 메시지를 지웁니다. nack는 처리 실패 또는 재시도를 뜻합니다. 이 구조 덕분에 소비자가 중간에 죽어도 메시지를 다시 처리할 수 있습니다.

데이터가 너무 많아지면 샤딩을 검토합니다

데이터베이스 복제는 읽기 부하를 줄이는 데 효과적입니다. 하지만 데이터 자체가 너무 많아지면 한 데이터베이스에 모두 담기 어려워집니다. 이때 샤딩을 고려합니다.

샤딩은 같은 종류의 데이터를 여러 데이터베이스에 나누어 저장하는 방식입니다.

user_id 1 ~ 10,000,000        -> 샤드 1
user_id 10,000,001 ~ 20,000,000 -> 샤드 2
user_id 20,000,001 이상        -> 샤드 3

샤드는 나뉜 데이터 조각을 담는 저장소입니다. 수평 샤딩은 같은 테이블의 행을 기준으로 나누는 방식입니다. 수직 샤딩은 사용자 도메인, 결제 도메인, 게시글 도메인처럼 기능 영역을 기준으로 데이터베이스를 나누는 방식입니다.

샤딩은 강력하지만 비용이 큽니다. 샤드 간 조인이 어려워지고, 어떤 기준으로 데이터를 나눌지 신중하게 정해야 합니다. 그래서 처음부터 샤딩을 선택하기보다 복제, 캐시, 인덱스, 도메인 분리로 버틸 수 있는지 먼저 확인하는 편이 좋습니다.

1장의 핵심은 장애 지점 제거와 역할 분리입니다

1장의 흐름을 한 문장으로 정리하면 다음과 같습니다.

사용자가 늘어날수록 병목을 찾고, 그 병목을 독립된 계층으로 분리하며, 한 곳의 장애가 전체 장애로 번지지 않게 만듭니다.

로드밸런서는 웹 서버 병목을 나눕니다. 데이터베이스 복제는 읽기 부하를 나눕니다. 캐시는 반복 조회를 줄입니다. CDN은 정적 파일을 사용자 가까이 둡니다. 메시지 큐는 오래 걸리는 작업을 요청 흐름 밖으로 뺍니다. 샤딩은 데이터 자체를 나눕니다.

각 기술을 따로 외우기보다 “지금 병목이 어디인가”, “이 선택이 어떤 장애 지점을 줄이는가”, “대신 어떤 복잡도를 추가하는가”를 함께 보는 편이 더 도움이 됩니다.

다음 글에서는 이 구조를 설계하기 전에 필요한 숫자 감각을 다룹니다. QPS, 저장소 용량, 가용성을 대략 계산하는 방법입니다.

이어 읽기

시리즈는 순서대로, 편집 추천은 맥락대로, 비슷한 주제는 태그 기준으로 정리합니다.

시리즈 전체

대규모 시스템 설계 스터디1/5
  1. 1.대규모 시스템 설계 스터디 01. 1명에서 수백만 명까지
  2. 2.대규모 시스템 설계 스터디 02. 개략적인 규모 추정
  3. 3.대규모 시스템 설계 스터디 03. 설계 질문을 풀어가는 순서
  4. 4.대규모 시스템 설계 스터디 04. 처리율 제한 장치
  5. 5.대규모 시스템 설계 스터디 05. 안정 해시

비슷한 주제의 글

태그가 겹치는 글입니다. 시리즈와 편집 추천에 이미 나온 글은 제외합니다.