블록 저장소(Block Storage) HDD나 SSD처럼 서버에 물리적으로 연결되는 형태의 저장소다. 원시 블록을 볼륨 형태로 서버에 제공하며, 데이터베이스나 가상 머신 엔진 같은 애플리케이션이 직접 블록을 제어해 최대 성능을 끌어낼 수 있다. 서버에 물리적으로 붙이는 것에만 국한되지 않고, FC(Fibre Channel)이나 iSCSI 같은 네트워크 프로토콜을 통해 연결하는 것도 블록 저장소 범주에 들어간다.
AWS에서는 EBS(Elastic Block Store)가 대표적이다.
파일 저장소(File Storage) 블록 저장소 위에 구현되는 추상화 계층이다. 파일과 디렉터리 형태의 계층적 구조를 제공하며, 블록을 직접 제어하고 볼륨을 포맷하는 등의 작업을 신경 쓸 필요가 없어 사용하기 편하다. SMB/CIFS나 NFS 같은 네트워크 프로토콜을 사용하면 하나의 저장소를 여러 서버에서 동시에 붙여 사용할 수도 있다.
객체 저장소(Object Storage) 데이터를 수평적인 구조 안에 객체로 보관하는 새로운 형태의 저장소다. 데이터 영속성을 높이고 대규모 애플리케이션을 지원하면서 비용을 낮추기 위해 의도적으로 성능을 희생한다. 실시간으로 갱신할 필요가 없는 차가운(cold) 데이터 보관에 초점을 맞추며, 데이터 아카이브나 백업에 주로 쓰인다. 모든 데이터 접근은 RESTful API를 통한다. AWS S3가 대표적이다.
스터디에서는 세 종류를 구분하면서, 분산 환경이 일반화되며 객체 저장소가 사실상 기본값처럼 자리잡게 된 이유를 이야기했다. 블록 저장소는 단일 서버에 강하게 묶이는 반면, 객체 저장소는 어디서든 글로벌 접근이 가능하다는 점이 분산 환경에서의 선택을 자연스럽게 만든다는 의견이 나왔다.
2. 용어 정리
버킷(Bucket) 객체를 보관하는 논리적 컨테이너다. 버킷은 전역적으로 유일해야 하며, S3에 객체를 업로드하려면 먼저 버킷을 생성해야 한다. S3에서 버킷 이름이 DNS로 직접 연결되기 때문에 전 세계에서 유일한 이름이어야 한다.
스터디에서는 이와 관련해 버킷 이름 하이재킹이나 버킷 스쿼팅(Bucket Squatting) 같은 보안 이슈가 언급되었다. 계정이 삭제된 이후 해당 버킷 이름을 다른 사람이 선점하는 방식의 공격 사례가 실제로 보고된 바 있다고 한다.
객체(Object) 버킷에 저장되는 개별 데이터다. 실제 데이터(페이로드)와 메타데이터로 구성된다. 객체 데이터로는 어떤 것도 저장 가능하며, 메타데이터는 객체를 기술하는 이름-값 쌍의 집합이다. 한 번 저장된 객체는 수정이 불가능하며(객체 불변성, Object Immutability), 삭제 후 새 버전으로 완전히 교체하는 방식으로만 변경할 수 있다.
버전(Versioning) 한 객체의 여러 버전을 같은 버킷 안에 보관하는 기능이다. 실수로 삭제하거나 덮어쓴 객체를 복구할 수 있도록 해준다. 객체를 삭제할 때는 실제로 지우는 것이 아니라 삭제 표식(delete marker)만 추가하는 방식으로 동작한다. 삭제 표식이 추가된 현재 버전에 GET 요청을 보내면 404가 반환된다.
3. 규모 추정
책 기준으로 100PB 데이터를 대상으로 설계한다.
디스크 용량 관점에서 객체 크기 분포를 다음과 같이 가정한다.
- 20%: 1MB 미만 소형 객체
- 60%: 1MB ~ 64MB 중형 객체
- 20%: 64MB 이상 대형 객체
소형 0.5MB, 중형 32MB, 대형 200MB 기준으로 계산하면 약 6억 8천만 개 객체가 나온다.
비기능 요구사항으로는 99.9999%(식스 나인) 수준의 데이터 내구성, 99.99%(포 나인) 수준의 서비스 가용성, 저장소 비용 최소화가 제시된다.
스터디에서 S3의 실제 규모 관련 수치도 공유되었다. 2013년 기준 2조 개, 2021년 기준 100조 개였던 저장 객체 수가 2025년 AWS 발표 기준 500조 개까지 증가했다고 한다. 단일 파일 최대 업로드 가능 크기도 현재는 50TB까지 확장되었다. (2025년 12월 업데이트 기준)
4. 개략적 설계안
개략적 설계는 크게 로드 밸런서, API 서비스, IAM 서비스, 데이터 저장소, 메타데이터 저장소로 구성된다.
1. 로드 밸런서: RESTful API 요청을 API 서버들에 분산한다.
2. API 서비스: IAM 서비스, 메타데이터 서비스, 데이터 저장소에 대한 호출을 조율하는 역할을 한다. 사용자의 요청을 받으면 인증/인가를 확인하고 각 서비스를 호출해 처리한다.
3. IAM 서비스: 인증(Authentication), 권한 부여(Authorization), 접근 제어(Access Control)를 중앙에서 처리한다.
4. 데이터 저장소: 실제 객체 데이터를 보관한다. 내부적으로 데이터 라우팅 서비스, 배치 서비스, 데이터 노드로 구성된다.
5. 메타데이터 저장소: 객체 메타데이터를 보관한다.
5. 객체 업로드 / 다운로드 흐름
객체 업로드 흐름
1. 클라이언트가 버킷 생성 HTTP PUT 요청을 보낸다.
2. API 서비스가 IAM을 호출해 WRITE 권한 여부를 확인한다.
3. 확인 완료 후 메타데이터 저장소에 버킷 정보를 등록한다.
4. 버킷 생성 후 클라이언트가 객체 업로드 HTTP PUT 요청을 보낸다.
5. API 서비스가 신원 및 WRITE 권한을 재확인한다.
6. 문제가 없으면 객체 데이터를 데이터 저장소로 전달한다.
7. 데이터 저장소가 객체를 저장하고 UUID를 반환한다.
8. API 서비스가 메타데이터 저장소에 object_id(UUID), bucket_id, object_name 등을 등록한다.
9. 200 OK 응답을 반환한다.
객체 다운로드 흐름
1. 클라이언트가 GET /bucket-name/object-name 요청을 보낸다.
2. API 서비스가 IAM을 통해 READ 권한을 확인한다.
3. 메타데이터 저장소에서 해당 객체의 UUID를 조회한다.
4. UUID로 데이터 저장소에서 실제 데이터를 가져온다.
5. 객체 데이터를 HTTP 응답으로 반환한다.
6. 데이터 저장소 상세 설계
데이터 저장소는 데이터 라우팅 서비스, 배치 서비스, 데이터 노드로 구성된다.
데이터 라우팅 서비스 데이터 노드 클러스터에 접근하기 위한 RESTful 또는 gRPC 서비스를 제공한다. 무상태 서비스라 서버를 추가해 쉽게 규모를 확장할 수 있다. 배치 서비스를 호출해 데이터를 저장할 최적의 노드를 결정하고, 데이터 노드에서 데이터를 읽어 API 서비스에 반환하는 역할을 한다.
배치 서비스 어느 데이터 노드에 데이터를 저장할지 결정하는 역할을 한다. 내부적으로 가상 클러스터 지도를 유지하며, 클러스터의 물리적 형상 정보를 바탕으로 데이터 사본이 물리적으로 다른 위치에 놓이도록 결정한다. 아주 중요한 서비스이기 때문에 5~7개 노드를 갖는 클러스터를 Paxos나 Raft 같은 합의 프로토콜로 구축하는 것이 권장된다.
데이터 노드 실제 객체 데이터가 보관되는 곳이다. 여러 노드에 데이터를 복제함으로써 안정성과 내구성을 보증하는데, 이를 다중화 그룹이라고 한다. 데이터 노드에는 배치 서비스에 주기적으로 박동 메시지(heartbeat)를 보내는 서비스 데몬이 동작하며, 부착된 드라이브 수, 각 드라이브에 저장된 데이터 양 등의 정보를 함께 전달한다.
데이터 저장 흐름
1. API 서비스가 객체 데이터를 데이터 라우팅 서비스로 전달한다.
2. 데이터 라우팅 서비스가 UUID를 할당하고 배치 서비스에 저장할 데이터 노드를 질의한다.
3. 배치 서비스가 가상 클러스터 지도를 확인해 주 데이터 노드를 반환한다.
4. 데이터 라우팅 서비스가 UUID와 함께 데이터를 주 데이터 노드에 전송한다.
5. 주 데이터 노드가 데이터를 자신에게 저장하고, 두 개의 부 데이터 노드에 다중화한다.
6. 모든 부 노드에 다중화가 완료되면 UUID를 API 서비스에 반환한다.
이 흐름에서 주목할 점은 모든 부 노드에 다중화가 완료된 후에야 응답을 반환한다는 것이다. 모든 데이터 노드에 강력한 일관성이 보장되는 대신, 가장 느린 사본의 작업이 완료될 때까지 응답을 반환하지 못하므로 지연 시간 측면에서는 손해가 된다. 데이터 일관성과 지연 시간 사이의 트레이드오프다.
7. 데이터는 어떻게 저장되는가
가장 단순한 방안은 객체를 개별 파일로 저장하는 것이지만, 작은 파일이 많아지면 두 가지 문제가 생긴다.
아이노드(inode) 한도 초과 문제 파일 시스템은 파일 위치 등의 정보를 아이노드라는 특별한 블록에 저장한다. 사용 가능한 아이노드 수는 디스크가 초기화되는 순간에 결정되기 때문에, 작은 파일이 수백만 개에 달하면 아이노드가 전부 소진될 가능성이 생긴다.
스터디에서는 하둡을 사용하는 환경에서 임시 파일이 과도하게 생성되어, 실제 용량은 몇십 KB에 불과한데도 파일 개수 한도에 걸려 담당자에게 긴급 알림이 왔던 경험담이 공유되었다.
낭비되는 블록 수 증가 문제 파일 시스템은 파일을 블록 단위로 저장한다. 작은 파일을 저장할 때도 블록 하나를 온전히 사용하기 때문에 작은 파일이 많으면 낭비되는 블록이 늘어난다.
스터디에서 4KB라는 블록 크기 기준에 대한 이야기도 나왔다. 리눅스와 윈도우 모두 메모리 페이지 크기가 4KB이기 때문에, 이 단위에 맞춰 블록 크기가 설정된 것으로 보인다는 의견이 나왔다.
이 문제를 해결하기 위해 책에서는 작은 객체들을 큰 파일 하나로 모아서 WAL(Write-Ahead Log)처럼 append하는 방식을 제안한다. 용량 임계치에 도달한 파일은 읽기 전용으로 변경하고 새 파일을 생성한다. 읽기-쓰기 파일에 대한 쓰기 연산은 순차적으로 이루어져야 하며, 코어별로 전담 읽기-쓰기 파일을 두어 쓰기 대역폭 저하를 방지한다.
object_mapping 테이블 많은 작은 객체가 하나의 파일에 섞여 있을 때 UUID로 객체 위치를 찾기 위해, 데이터 노드마다 object_mapping 테이블을 로컬에 유지한다.
- object_id: 객체의 UUID
- file_name: 객체를 보관하는 파일의 이름
- start_offset: 파일 내 객체의 시작 주소
- object_size: 객체의 바이트 단위 크기
이 테이블의 데이터는 한 번 기록된 후에는 변경되지 않으며 읽기가 매우 빈번하다. 쓰기 성능이 좋은 RocksDB(SSTable 기반)와 읽기 성능이 좋은 관계형 데이터베이스(B+ 트리 기반) 중, 쓰기보다 읽기가 많은 특성상 관계형 데이터베이스가 나은 선택이다. 데이터 노드 위치 데이터는 다른 노드와 공유할 필요가 없으므로 데이터 노드마다 로컬 관계형 데이터베이스를 두는 방식으로 구성한다.
1. 객체 이름으로 객체 ID 찾기
2. 객체 이름에 기반해 객체 삽입 또는 삭제
3. 같은 접두어를 갖는 버킷 내 모든 객체 목록 확인
버킷 테이블의 규모 확장 사용자가 만들 수 있는 버킷 수에 제한이 있어 테이블 크기 자체는 작다. 단일 서버에 충분히 저장 가능하지만, 읽기 부하 분산이 필요한 경우 사본을 만들어 처리한다.
객체 테이블의 규모 확장 객체 메타데이터는 단일 서버에 보관하기 불가능하므로 샤딩이 필요하다. bucket_id 기준으로 샤딩하면 특정 버킷에 데이터가 집중되는 핫스팟 문제가 생긴다. 본 설계안에서는 bucket_name과 object_name을 결합해 샤딩 기준으로 삼는다. 대부분의 메타데이터 연산이 객체 URI를 기준으로 하기 때문이다.
버킷 내 객체 목록 확인 S3는 접두어(prefix)라는 개념을 제공해 디렉터리와 비슷하게 데이터를 정리할 수 있다.
메타데이터 테이블을 샤딩하면 목록 조회 시 어떤 샤드에 데이터가 있는지 알 수 없으므로, 모든 샤드에 질의를 돌린 뒤 결과를 취합해야 한다. 페이지 나눔 구현도 복잡해지는데, 객체가 여러 샤드에 나눠져 있어 샤드마다 반환하는 객체 수가 다르기 때문이다. 서버는 모든 샤드의 오프셋을 추적해 커서(cursor)에 결부시켜야 한다.
10. 멀티파트 업로드(Multipart Upload)
크기가 큰 객체를 직접 업로드하면 시간이 오래 걸리고, 중간에 네트워크 문제가 생기면 처음부터 다시 업로드해야 한다. 멀티파트 업로드는 객체를 작은 조각으로 나눠 독립적으로 업로드한 뒤 모든 조각이 완료되면 원본 객체로 복원하는 방식이다.
스터디에서는 AWS가 단순히 파일을 쪼개는 것을 넘어, 각 파트 요청마다 다른 서버가 매핑되어 병렬로 처리되도록 설계되어 있다는 이야기가 나왔다.
S3에서 여러 파일을 웹 브라우저로 한꺼번에 다운로드할 수 없는 이유도 함께 이야기되었다. Google Drive는 앞단 서버가 여러 파일을 zip으로 묶어서 제공하지만, S3는 각 객체 자체가 엔드포인트이기 때문에 그런 레이어가 없다는 점, HTTP 1.1 기준 브라우저의 동시 커넥션 제한(6개) 같은 브라우저 정책 문제도 복합적으로 작용한다는 이야기가 나왔다.