Search

15장. 구글 드라이브 설계

생성일
2025/09/27 00:47
태그

개요

구글 드라이브, 드롭박스, 마이크로소프트 원드라이브, 애플 아이클라우드 등 클라우드 저장소를 통해 파일을 저장하고 동기화하여 원하는 문서, 사진, 비디오 등의 파일들을 클라우드에 보관할 수 있는 서비스를 제공한다.
이번 장에서는 그 중 구글 드라이브 서비스를 설계해보자.

1단계 문제 이해 및 설계 범위 확정

설계 범위 좁히기

구글 드라이브를 설계하는 것은 큰 프로젝트이니, 질문을 통해 설계 범위를 좁혀야한다.
지원자: 가장 중요하게 지원해야 할 기능들은 무엇인가요? 면접관: 파일 업로드/다운로드, 파일 동기화, 그리고 알림(notification)입니다. 지원자: 모바일 앱니아 웹 앱 중에 하나만 지원하면 되나요, 아니면 둘 다 지원해야 하나요? 면접관: 둘 다 지원해야 합니다. 지원자: 파일을 암호화 해야할까요? 면접관: 네. 지원자: 파일 크기에 제한이 있습니까? 면접관: 10GB 제한이 있습니다. 지원자: 사용자는 얼마나 됩니까? 면접관: 일간 능동 사용자(DAU) 기준으로 천만(10 Million) 명입니다.
Plain Text
복사
이를 통해 좁힌 설계 범위로 다음 기능들의 설계에 집중할 것이다.
파일 추가 : 가장 쉬운 방법으로는 drag-and-drop이 있다.
파일 다운로드
파일 동기화 : 한 단말에서 파일을 추가하면 다른 단말에서도 자동으로 동기화되어야 한다.
파일 갱신 이력 조회(revision history)
파일 공유
알림 : 파일이 편집되거나 삭제되거나 새롭게 공유되었을 때 알림 표시
추가적으로 다음기능에 대해서는 논의하지 않을 것이다.
구글 문서(Google doc) 편집 및 협업(collaboration) 기능 : 구글 문서는 여러 사용자가 동시에 편집할 수 있도록 하는데, 이 부분은 설계 범위에서 제외한다.
기능적 요구사항 외에, 다음의 비-기능적 요구사항을 이해하는 것도 중요하다.
안정성 : 저장소 시스템에서 안정성은 아주 중요하고 데이터 손실은 발생해서는 안된다.
빠른 동기화 속도 : 파일 동기화에 시간이 너무 많이 걸리면 사용자는 해당 제품을 더 이상 사용하지 않을 것이다.
네트워크 대역폭 : 네트워크 대역폭을 불필요하게 많이 소모한다면, 모바일 데이터 플랜을 사용하는 등의 상황에서 사용자에게 별로 좋지 않은 경험을 제공할 것이다.
규모 확장성 : 이 시스템은 아주 많은 양의 트래픽도 처리 가능해야한다.
높은 가용성 : 일부 서버에 장애가 발생하거나, 느려지거나, 네트워크 일부가 끊겨도 시스템은 계속 사용 가능해야한다.

개략적 추정

가입 사용자는 5천만(50 Million) 명이고, 천만 명의 DAU 사용자가 있다고 가정
모든 사용자에게 10GB의 무료 저장공간 할당
매일 각 사용자가 평균 2개의 파일을 업로드한다고 가정.
사용자가 업로드하는 각 파일의 평균 크기는 500KB
읽기:쓰기 비율은 1:1
필요한 저장공간 = 5천만 명 * 10GB = 500PB(PetaByte)
업로드 API QPS = 1천만 명 * 2회 / 24시간 / 3600초 = 약 240
최대(Peek) QPS = QPS * 2 = 480

2단계 개략적 설계안 제시 및 동의 구하기

이번 장에서는 모든 것을 한 대 서버에 담아서 출발해, 점진적으로 천만 사용자 지원이 가능한 시스템으로 발전시켜 나갈 것이다. 먼저 한 대의 서버로 시작하기 위해서는 다음과 같은 컴포넌트가 필요할 것이다.
파일을 올리고 다운로드 하는 과정을 처리할 웹 서버
사용자 데이터, 로그인 정보, 파일 정보 등 메타베이터를 보관할 데이터베이스
파일을 저장할 저장소 시스템 1TB
아파치 웹 서버를 설치하고, MySQL 데이터베이스를 깔고, 업로드되는 파일을 저장할 drive/라는 디렉터리들을 준비한다. drive 내에는 네임스페이스(namespcae)라 불리는 하위 디렉터리들을 둔다.
위처럼 각 네임스페이스 안에는 특정 사용자가 올린 파일이 보관된다. 파일은 원래의 파일명과 동일하고, 각 파일과 폴더는 그 상대 경로를 네임스페이스 이름과 결합하여 유일하게 식별할 수 있게 된다.

API

이 시스템에서는 기본적으로 파일 업로드, 다운로드, 파일 갱신 히스토리 조회의 3가지 API가 필요하다. 아래의 API들은 주고받는 데이터를 보호하기 위해, 기본적으로 SSL(Secure Socket Layer)를 지원하는 HTTPS 프로토콜을 사용해야한다.

파일 업로드 API

다음 두 가지 종류의 업로드를 지원할 것이다.
단순 업로드 : 파일 크기가 작을 때
이어 올리기(resumable upload) : 파일 사이즈가 크고 네트워크 문제로 업로드가 중단될 가능성이 높다고 판단되면 사용
이어 올리기 예시
https://api.example.com/ fileslupload?uploadType=reSUiflable
인자 - 업로드할 로컬 파일
절차
이어 올리기 URL을 받기 위한 최초 요청
데이터 업로드하고 업로드 상태 모니터링
업로드에 장애가 발생하면 장애 발생 시점부터 업로드를 재시작

파일 다운로드 API

파일 다운로드 예시
https://api.example.com/ files/download
인자 - 다운로드할 파일의 경로
{ "path": "/recipes/soup/best_soup.txt" }
JSON
복사

파일 갱신 히스토리 조회 API

파일 갱신 히스토리 조회 예시
https:I/api.example.com/ files/list_revisions
인자 - 갱신 히스토리를 가져올 파일의 경로와 히스토리 길이 최대치
{ "path": "/recipes/soup/best_soup.txt", "limit": 20 }
JSON
복사

한 대 서버의 제약 극복

업로드 파일이 많아지면 결국 파일 시스템은 가득차게 된다.
이렇게 되면 사용자는 더 이상 파일을 올릴 수 없게 되니, 긴급하게 문제를 해결해야 한다. 가장 먼저 떠오르는 해결책은 데이터를 샤딩하여 여러 서버에 나누어 저장하는 것이다.
유저 id를 통한 샤딩으로 위와 같이 구성하면 다시 사용자는 파일을 업로드 할 수 있고 다시 안정적으로 서비스를 제공할 것이다.
하지만 서버에 장애가 생기면 데이터를 잃게 되는 문제가 여전히 남아있으니, 넷플릭스나 에어비엔비 같은 시장 주도 기업들에서도 저장소로 사용하는 아마존 S3를 고려해볼 필요가 있다. 아마존 S3는 업계 최고 수준의 규모 확장성, 가용성, 보안, 성능을 제공하는 객체 저장소 서비스이다. 그러니 우리의 서비스에도 S3를 사용해 파일을 저장하자.
S3는 다중화를 지원하는데, 같은 지역 안에서 다중화를 할 수 있고 여러 지역에 걸쳐 다중화 할 수도 있다. AWS region은 AWS가 데이터 센터를 운영하는 지리적 영역이다. 이 region을 통해 다중화를 어떤 지역에 할 지 선택할 수 있다.
오른쪽처럼 여러 지역에 걸쳐 다중화를 한다면, 데이터 손실을 막고 가용성을 최대한 보장할 수 있으므로 우리 설계에도 여러 지역에 다중화를 도입한다.
이제 데이터 손실에 대한 걱정을 덜어둘 수 있다. 하지만 다음과 같은 부분에 더 개선이 필요할 수 있다.
로드밸런서 : 네트워크 트래픽을 분산하기 위해 로드밸런서를 사용한다. 로드밸런서는 트래픽을 고르게 분산할 뿐만 아니라, 특정 웹 서버에 장애 발생 시 해당 서버를 자동으로 우회해준다.
웹 서버 : 로드밸런서를 추가하고나면 더 많은 웹 서버를 손쉽게 추가할 수 있다. 이를 통해 트래픽이 폭증해도 쉽게 대응이 가능하다.
메타데이터 데이터베이스 : 데이터베이스를 파일 저장 서버에서 분리하여 SPOF(Single Point of Failure)를 회피한다. 그에 더해 다중화 및 샤딩 정책을 적용하여 가용성과 규모 확장성에 대응한다.
파일 저장소 : S3를 파일 저장소로 사용하고 가용성과 데이터 무손실을 보장하기 위해 두 개 이상 지역에 데이터를 다중화한다.
이 모든 부분을 개선하고나면 웹 서버, 메타데이터 데이터베이스, 파일 저장소가 위 그림과 같이 잘 분리되어있을 것이다.

동기화 충돌

구글 드라이브 같은 대형 저장소 시스템은 두 명 이상의 사용자가 같은 파일이나 폴더를 동시에 업데이트하려고 하는 경우 동기화 충돌이 발생할 수 있다. 이런 충돌을 해결하기 위해 이번 설계에서는 먼저 처리되는 변경을 성공으로 간주하고, 나중에 처리되는 변경은 충돌이 발생한 것으로 표시하도록 처리할 것이다.
위 그림의 동기화 충돌 오류가 발생한 사용자 2는 로컬 사본(local copy)과 서버에 있는 최신 버전의 파일로 두 가지 버전의 파일이 존재한다.
이 상태에서 두 파일을 하나로 합칠지 아니면 둘 중 하나를 다른 파일로 대체할지를 결정해야한다.

개략적 설계안

사용자 단말 : 사용자가 이용하는 웹브라우저나 모바일 앱 등의 클라이언트이다.
블록 저장소 서버 : 파일 블록을 클라우드 저장소에 업로드하는 서버로, 블록 수준 저장소(block-level storage)라고도 불린다.
클라우드 환경에서 데이터 파일을 저장하는 기술로, 파일을 여러 개의 블록으로 나눠 각 블록에 고유한 해시값을 할당하여 저장하고 이 해시값이 메타데이터 데이터베이스에 저장된다.
각 블록은 독립적인 객체로 취급되고, 블록들을 원래 순서대로 합쳐 파일을 재구성한다.
드롭박스는 블록 크기 최대 4MB 제한
클라우드 저장소 : 블록 단위로 나뉜 파일이 저장되는 저장소다.
아카이빙 저장소 : 오랫동안 사용되지 않은 비활성 데이터를 저장하기 위한 컴퓨터 시스템이다.
로드밸런서 : 요청을 모든 API 서버에 고르게 분산하는 역할을 한다.
API 서버 : 파일 업로드 외의 사용자 인증, 프로필 관리, 파일 메타데이터 갱신 등 거의 모든 기능을 담당하여 처리하는 서버이다.
메타데이터 데이터베이스 : 사용자, 파일, 블록, 버전 등 메타데이터 정보를 관리한다.
메타데이터 캐시 : 성능을 높이기 위해 자주 사용되는 메타데이터를 캐싱한다.
알림 서비스 : 파일 추가, 편집, 삭제 등 특정 이벤트가 발생했을 때 클라이언트에 알림을 전송하는 pub/sub 프로토콜 기반 시스템이다.
오프라인 사용자 백업 큐 : 클라이언트가 접속 중이 아니라서 파일의 최신 상태를 확인할 수 없을 때, 해당 정보를 이 큐에 두어 나중에 클라이언트가 접속했을 때 동기화하도록 처리한다.

3단계 상세 설계

블록 저장소 서버, 메타데이터 데이터베이스, 업로드 절차, 다운로드 절차, 알림 서비스, 파일 저장소 공간 및 장애 처리 흐름에 대해 자세히 알아보자.

블록 저장소 서버

정기적으로 갱신되는 큰 파일들을 업데이트가 생길 때마다 전체 파일을 서버로 보내면 네트워크 대역폭을 많이 잡아먹게 된다. 이를 최적화 하는 방법은 두 가지 정도를 생각해볼 수 있다.
델타 동기화 : 파일이 수정되면 전체 파일 대신 수정이 일어난 블록만 동기화하는 것이다.
압축 : 블록 단위로 압축해두면 데이터 크기를 많이 줄일 수 있다. 텍스트 파일은 gzip이나 bzip2, 이미지나 비디오 압축 시에는 ffmpeg 등 파일 유형에 따라 압축 알고리즘을 선택한다.
우리가 설계한 시스템에서 블록 저장소는 파일 업로드에 관계된 컴포넌트인데, 클라이언트가 보낸 파일을 블록 단위로 나누고, 각 블록에 압축 알고리즘을 적용하고, 암호화까지 수행 해야한다. 추가로 전체 파일을 저장소에 보내는 대신 수정된 블록만 전송해야한다.
주어진 파일을 작은 블록들로 분할
각 블록을 압축
암호화
클라우드 저장소로 전송
위 그림에서 검정색 블록은 수정된 블록으로, 갱신된 부분만 동기화 하여 클라우드 저장소에 업로드한다.

높은 일관성 요구사항

파일을 저장하는 시스템은 강한 일관성 모델을 기본으로 지원해야 한다. 같은 파일이 단말이나 사용자에 따라 다르게 보이는 것이 허용될 수 없다는 의미이다. 메타데이터 캐시와 데이터베이스도 마찬가지이다.
메모리 캐시는 보통 결과적 일관성 모델을 지원하는데, 우리 시스템에서 강한 일관성을 달성하려면 다음 사항을 보장해야한다.
캐시에 보관된 사본과 데이터베이스에 있는 원본이 일치한다.
데이터베이스에 보관된 원본에 변경이 발생하면 캐시에 있는 사본을 무효화한다.
관계형 데이터베이스는 ACID를 보장하므로 강한 일관성을 보장하기 쉽지만, NoSQL 데이터베이스는 기본적으로 이를 지원하지 않아 동기화 로직 안에 프로그램해서 넣어야한다. 우리는 관계형 데이터베이스를 채택해 높은 일관성 요구사항에 대응할 것이다.

메타데이터 데이터베이스

위 그림은 중요한 것만 간추린 단순화된 형태의 데이터베이스 스키마이다.
user : user 테이블은 이름, 이메일, 프로필 사진 등 사용자에 관계된 기본적 정보들이 보관된다.
device : device 테이블에는 단말 정보가 보관된다. 한 사용자가 여러 대의 단말을 가질 수 있음에 유의하자.
namespace : namespace 테이블은 사용자의 루트 디렉토리 정보가 보관된다.
file : file 테이블에는 파일의 최신 정보가 보관된다.
file_version : 파일의 갱신 이력이 보관되는 테이블이다. 이 테이블에는 갱신 이력이 훼손되는 것을 막기 위해 읽기 전용 레코드가 저장된다.
block : 파일 블록에 대한 정보를 보관하는 테이블이다. 특정 보전의 파일의 블록들을 올바른 순서로 조합하면 복원해 낼 수 있다.

업로드 절차

위 그림은 두 개의 요청이 병렬적으로 전송된 상황을 보여준다. 첫 번째 요청은 파일 메타데이터를 추가하는 요청이고, 두 번째 요청은 파일을 클라우드 저장소로 업로드하기 위한 요청이다.
파일 메타데이터 추가
1.
클라이언트 1이 새 파일의 메타데이터 추가를 위해 요청 전송
2.
새 파일의 메타데이터를 데이터베이스에 저장하고 업로드 상태를 pending으로 변경
3.
새 파일이 추가되었음을 알림 서비스에 전달
4.
알림 서비스는 미리 등록된 클라이언트 2에게 파일이 업로드 되었음을 알림
파일을 클라우드 저장소에 업로드
1.
클라이언트 1이 파일을 블록 저장소 서버에 업로드 요청
2.
블록 저장소 서버는 파일을 블록 단위로 쪼갠 다음 압축하고 암호화하여 클라우드 저장소에 전송
3.
업로드가 끝나면 클라우드 스토리지는 API 서버로 완료 콜백(callback)을 호출
4.
메타데이터 DB에 저장된 해당 파일의 상태를 uploaded로 변경
5.
알림 서비스에 파일 업로드가 끝났음을 통지
6.
알림 서비스는 클라이언트 2에게 파일 업로드가 끝났음을 알림
위 흐름은 파일 수정 절차도 비슷하다.

다운로드 절차

파일이 새로 추가되거나 편집되면, 파일 다운로드는 자동으로 시작된다. 다른 클라이언트에서 발생한 파일 편집이나 추가된 사실을 감지하는 방법은 두 가지가 있다.
클라이언트 A가 접속 중이고 다른 클라이언트가 파일을 변경하면, 알림 서비스가 클라이언트 A에게 변경이 발생했으니 새 버전을 끌어가야 한다고 알린다.
클라이언트 A가 네트워크에 연결된 상태가 아닐 경우에는, 캐시에 데이터를 보관했다가 해당 클라이언트가 접속 중으로 바뀌면 그 때 새 버전을 가져간다.
파일이 변경됨을 감지한 클라이언트는 API 서버를 통해 메타데이터를 새로 가져가야하고, 그 후 블록들을 다운받아 파일을 재구성해야한다.
1.
알림 서비스가 클라이언트 2에게 누군가 파일을 변경했음을 알림
2.
알림을 확인한 클라이언트 2는 새로운 메타데이터를 요청
3.
API 서버는 메타데이터 DB에게 새 메타데이터 요청
4.
API 서버가 새 메타데이터를 받음
5.
클라이언트 2에게 새 메타데이터 반환
6.
클라이언트 2는 새 메타데이터를 받는 즉시 블록 다운로드 요청 전송
7.
블록 저장소 서버가 클라우드 저장소에서 블록 다운로드
8.
클라우드 저장소는 블록 서버에 요청된 블록 반환
9.
블록 저장소 서버는 클라이언트에게 요청된 블록을 반환
10.
클라이언트 2는 응답 받은 블록을 사용해 파일 재구성

알림 서비스

파일의 일관성을 유지하기 위해, 클라이언트는 로컬에서 파일이 수정됨을 감지하는 순간 다른 클라이언트에 그 사실을 알려 충돌 가능성을 줄여야한다. 알림 서비스는 이 목적을 위해 이벤트 데이터를 클라이언트로 보내는 서비스이다. 따라서 다음 두 가지의 선택지가 있다.
롱 폴링 : 클라이언트가 주기적으로 서버에 확인 요청(드롭박스가 채택 중인 방식)
웹소켓 : 클라이언트와 지속적인 통신 채널로 이벤트 전송
롱 폴링과 웹소켓 둘 다 좋은 방식이지만, 이번 설계에서는 다음과 같은 이유로 롱 폴링을 사용할 것이다.
채팅 서비스와 달리 본 시스템에서는 알림 서비스와 클라이언트 간 양방향 통신이 필요하지 않고 서버에서 클라이언트로의 통신만 요구된다.
구글 드라이브는 알림을 보낼 일이 그리 자주 발생하지 않고, 알림을 보내야하는 경우에도 단시간에 많은 데이터를 보낼 일은 없다. 실시간 양방향 통신이 요구되는 상황이 아니면 웹소켓은 적합하지 않다.
롱 폴링을 사용하면, 각 클라이언트는 알림 서버와 롱 폴링용 연결을 유지하다가 특정 파일에 대한 변경을 감지하면 해당 연결을 끊는다. 이때 클라이언트는 서버와 통신해 최신 내역을 다운로드 해야만 하고, 다운로드가 끝났거나 연결 타임아웃이 발생한 경우 즉시 새 요청을 보내 롱 폴링 연결을 복원하고 유지해야 한다.

저장소 공간 절약

파일 갱신 이력을 보존하고 안정성을 보장하려면, 파일의 여러 버전을 여러 데이터센터에 보관할 필요가 있다. 하지만 모든 버전을 자주 백업하면 저장용량이 너무 빨리 소진되니 다음의 세 가지 방법을 사용해 비용을 절감한다.
중복 제거(de-dupe) : 중복된 파일 블록을 계정 차원에서 제거하는 방법. 해시값을 비교해 두 블록의 동등성을 판단한다.
지능적 백업 전략 도입 :
한도 설정 : 보관해야하는 파일 버전 개수에 상한을 두고, 상한에 도달하면 오래된 버전 버리기
중요한 버전만 보관 : 편집 중인 문서가 업데이트 될 때마다 새로운 버전으로 관리한다면 짧은 시간 동안 1000개가 넘는 버전이 만들어질 수도 있다. 불필요한 버전과 사본이 만들어지는 것을 피하기 위해 중요한 것만 골라 저장한다.
아카이빙 저장소 활용 : 몇 달 혹은 몇 년 사용되지 않는 데이터를 아카이빙 저장소(cloud storage)로 옮긴다. 아마존 S3 글래시어(glacier) 같은 아카이빙 저장소는 S3보다 훨씬 저렴하다.

장애 처리

장애는 대규모 시스템이라면 피할 수 없기 때문에, 설계 시 이러한 점을 반드시 고려해야한다.
로드밸런서 장애 : 로드밸런서에 장애가 발생할 경우 부(secondary) 로드밸런서가 활성화되어 트래픽을 이어받아야 한다. 로드밸런서끼리 박동(heartbeat) 신호를 주기적으로 보내서 상태를 모니터링하고, 일정 시간 동안 박동 신호에 응답이 없다면 장애가 발생한 것으로 간주한다.
블록 저장소 서버 장애 : 블록 저장소 서버에 장애가 발생하면 다른 서버가 미완료 상태 혹은 대기 상태인 작업을 이어받아 처리해야 한다.
클라우드 저장소 장애 : S3 버킷은 여러 지역에 다중화할 수 있으므로, 한 지역에서 장애가 발생하면 다른 지역에서 파일을 가져오도록 한다.
API 서버 장애 : API 서버들은 무상태 서버이니, 로드밸런서가 트래픽을 해당 서버로 보내지 않도록 우회하여 장애 서버를 격리한다.
메타데이터 캐시 장애 : 메타데이터 캐시 서버도 다중화하여, 하나의 노드에 장애가 발생해도 다른 노드에서 데이터를 가져올 수 있도록 한다. 장애가 발생한 서버는 새 서버로 교체한다.
메타데이터 DB 장애 :
주 데이터베이스 서버의 장애는 부 데이터베이스 서버 중 하나를 주 서버로 올리고 부 서버를 하나 추가한다.
부 데이터베이스 서버의 장애는 다른 부 서버가 읽기 연산을 처리하고, 그동안 새로운 부 데이터베이스 서버를 새 것으로 교체한다.
알림 서비스 장애 : 접속 중인 모든 사용자는 알림 서버와 롱 폴링 연결을 하나씩 유지한다. 때문에 알림 서비스는 많은 사용자와 연결을 유지하고 관리하는데, 한 대의 서버에 장애가 발생하면 백만 명 이상의 롱 폴링 연결을 다시 만들어야 할 수도 있다. 한 대의 서버가 백만 개의 접속을 유지하는 것은 가능하지만, 백만 개의 접속을 시작하는 것은 불가능하여 롱 폴링 연결을 복구하는 것은 느릴 수 밖에 없다.
오프라인 사용자 백업 큐 장애 : 이 큐 또한 다중화하여, 큐에 장애가 발생하면 구독 중인 클라이언트들을 다른 백업 큐로 구독 관계를 재설정해야 한다.

4단계 마무리

설계를 마치고 시간이 조금 남는다면, 설계안에서 어떤 다른 선택지가 있었는 지를 논의해보면 좋다.
블록 저장소 서버를 거치지 않고 파일을 클라우드 저장소에 직접 업로드 했을 경우
장점
서버를 안거치니 업로드 시간이 빨리진다.
단점
분할과 압축, 암호화 로직을 클라이언트에 두어야하므로 플랫폼(iOS, 안드로이드, 웹 등) 별로 따로 구현 필요
클라이언트가 해킹 당할 수 있기 때문에 암호화 로직을 클라이언트에 두는 것은 좋지 않다.
접속 상태를 관리하는 로직을 별도의 서비스로 분리하는 것도 추가적으로 고려해볼만 하다. 관련 로직을 알림 서비스에서 분리하면, 다른 여러 서비스에서도 쉽게 접속 상태를 활용할 수 있게 되므로 좋다.