🔥 버킷과 키: 플랫 네임스페이스의 의미
강의 목차

콘솔에서 New folder 버튼을 눌러 users 라는 폴더 한 줄을 만들었다. 그런데 같은 대상을 aws s3api list-objects-v2 --bucket my-app-uploads-2026 로 들여다보니 폴더라는 단어는 응답 어디에도 없었다. Contents[] 배열에 객체 한 줄, Key 는 users/, Size 는 0. 콘솔이 보여 준 폴더 트리와 API가 돌려 준 한 줄짜리 객체가 분명 같은 대상을 가리키고 있는데, 두 단어가 가리키는 형태는 달랐다.
S3란 무엇인가: 오브젝트 스토리지의 구조 마지막 줄에 평면 네임스페이스라는 한 단어를 두었다. 슬래시는 디렉토리 구분자가 아니라 키 한 줄 안의 문자라는 한 명제. 이번 글은 그 명제 위에 운영 한 채를 쌓는다. 콘솔이 폴더라고 부르는 것이 실제로 무엇인지, 한 번의 ListObjectsV2 호출이 어떤 형식으로 폴더 환상을 빚어내는지, 그리고 그 평면이라는 구조가 prefix 자동 분할, 초당 3,500 PUT/5,500 GET, virtual-hosted URL이라는 세 곳에서 어떻게 운영에 닿는지.
콘솔의 'folder'는 0바이트 객체
Using folders 문서가 콘솔 동작을 한 줄로 끝낸다.
When you create a folder in Amazon S3 console, S3 creates a 0-byte object. This object key is set to the folder name that you provided plus a trailing forward slash (
/) character.
photos 라는 폴더를 콘솔에서 만들면 S3가 만드는 건 키 photos/, 크기 0바이트짜리 객체 한 행에 그친다. mkdir 같은 시스템 호출이 따로 있는 게 아니라 일반 객체 한 줄이 추가될 뿐이다. AWS 문서가 내부 API 이름까지 적어 두지는 않지만, 콘솔이 만든 결과물은 다른 객체와 똑같이 CLI/SDK/REST API로 보이고 만질 수 있다. 한국어로 다시 풀면 폴더가 만들어진 게 아니라 키가 users/ 인 빈 객체 한 줄이 늘어난 셈이다.

왜 이렇게 만들었나. S3 데이터 모델이 평면 네임스페이스라서다. 디렉토리라는 개념 자체가 데이터 모델에 없으면, 콘솔이 폴더를 보여 주려면 어떤 신호 같은 것이 필요했다. 그 신호가 trailing 슬래시로 끝나는 0바이트 객체. 콘솔이 객체 목록을 받아 와서 슬래시로 끝나는 키를 만나면 그걸 폴더로 해석해 트리 한 줄을 보여준다.
운영 측면에서 한 가지가 따라온다. 그 0바이트 객체도 다른 객체와 똑같이 CLI·SDK·REST API에서 보이고 다루는 일반 객체다. S3 Standard 가격으로 청구되긴 하나, 한 객체에 월 약 0.023 USD/GB 의 storage 가격이지만 0바이트라 storage 비용은 0에 가깝다. 다만 PUT/LIST 호출은 정상가다. 한 버킷에 폴더 100개를 만들어 두면 0바이트 객체 100개가 데이터 객체와 함께 LIST에 섞여 돌아오고, 콘솔이 보여 주는 트리도 그 객체들을 한 번 더 가져온다. 그래서 진짜 데이터 객체가 100만 개 있는 버킷에 콘솔 폴더 100개가 더 들어 있는 건 비용 면에서 별일 아니지만, 폴더 1만 개를 자동으로 찍어 내는 스크립트는 다시 살펴봐야 한다. 별 효용 없는 0바이트 객체 1만 개가 LIST 응답에 섞이게 된다.
ListObjectsV2가 'folder' 형태로 답하는 법
콘솔이 폴더를 보여 주는 진짜 도구는 ListObjectsV2 의 Delimiter 파라미터다. AWS 공식 문구 그대로.
Causes keys that contain the same string between the
prefixand the first occurrence of the delimiter to be rolled up into a single result element in theCommonPrefixescollection. These rolled-up keys are not returned elsewhere in the response. Each rolled-up result counts as only one return against theMaxKeysvalue.
세 단어로 풀면 Prefix(어느 지점부터 보겠다), Delimiter(어디서 자르겠다), CommonPrefixes(잘린 묶음). 여기서 Delimiter 를 / 로 주면 콘솔의 폴더 동작이 그대로 나온다.
한 사례로 짚는다. 버킷에 다음 키 다섯 개가 있다고 해 보자.
users/u-12345/avatar.png
users/u-12345/banner.png
users/u-67890/avatar.png
posts/2026/01/hello.md
posts/2026/02/intro.mdusers/u-12345/avatar.png
users/u-12345/banner.png
users/u-67890/avatar.png
posts/2026/01/hello.md
posts/2026/02/intro.md여기에 Prefix=users/, Delimiter=/ 로 ListObjectsV2를 호출하면 응답이 두 갈래로 나뉜다. Contents[] 는 비어 있고 (정확히는 users/ 자체에 0바이트 객체가 있다면 한 줄), CommonPrefixes 에 users/u-12345/ 와 users/u-67890/ 두 줄이 들어온다. 키 세 개가 마치 폴더 두 개처럼 묶여 답으로 돌아온 것이다. 그 다음 단계로 Prefix=users/u-12345/ 로 한 번 더 호출하면 그제서야 avatar.png 와 banner.png 가 Contents[] 에 그대로 들어온다.

다른 사례. 같은 호출에서 Delimiter 만 빼면 어떻게 되나. 한 페이지에 객체 다섯 개가 그대로 평면으로 나열돼 돌아온다. users/u-12345/avatar.png, users/u-12345/banner.png, ... 한 줄씩 키 전체가 Contents[] 에 그대로 들어와 있다. 콘솔이 폴더를 보여줄 때와 서버가 키 전체를 한 번에 처리할 때, 두 형태가 같은 API의 두 사용 패턴이다.
MaxKeys 는 한 호출당 기본·최대 모두 1,000 이고, 그 이상은 NextContinuationToken 을 받아 페이지네이션해야 한다. AWS 문구로 "KeyCount will always be less than or equal to the MaxKeys field". 그래서 객체 100만 개짜리 버킷을 통째로 LIST 하려면 최소 1,000번의 호출이 필요하고, 호출당 LIST 가격(AWS 공식 가격 페이지 기준 LIST는 PUT/COPY/POST와 같은 요율로 청구되며 리전별로 다르고, 게시 시점의 정확한 숫자는 S3 Pricing 에서 자기 리전을 확인)을 그만큼 곱한다. 그날 처음 그 청구서를 봤을 때, 데이터 자체보다 LIST 호출이 더 비싼 케이스가 있다는 걸 처음 알았다.
평면이라는 구조가 만든 운영 모형
콘솔이 폴더 환상을 만들 수 있다고 해서 진짜 디렉토리가 있는 것처럼 다룰 수는 없다. 한 대목에서 그 갭이 가장 분명하다. aws s3 mv s3://bucket/users/ s3://bucket/members/ 같은 명령이다.
같은 명령이 로컬 파일시스템이라면 inode 한 줄을 옮기는 일이지만, S3에서는 users/ prefix 안에 있는 모든 객체를 한 줄씩 CopyObject 로 새 키에 복사한 다음 DeleteObject 로 원본을 지우는 N×Copy + N×Delete 과정이 된다. AWS CLI mv 레퍼런스와 Copying objects가 그렇게 명시한다. 객체 1만 개가 있는 prefix를 rename하면 호출 2만 번. 시간도 시간이지만 청구서에 CopyObject 1만 건 + DeleteObject 1만 건이 한 줄씩 정직하게 올라간다. PUT/COPY 호출비는 리전별로 다르지만 일반 리전 기준 1,000건당 한 자릿수 센트 수준이라 호출비 자체는 무겁지 않은 편이다. 다만 storage 가격이 잠깐 두 배가 되는 구간이 더 무서울 수 있다 (복사 직후·삭제 직전 한 시점). 정확한 리전 가격은 S3 Pricing 에서 자기 리전을 직접 확인한다.
이 대목이 S3란 무엇인가: 오브젝트 스토리지의 구조 keyThread 중 "언제 이 서비스를 쓰지 말아야 하는가" 와 처음으로 맞닿는 곳이다. 한 디렉토리 통째로 rename이 일주일에 몇 번씩 일어나는 워크로드라면, 예를 들어 사용자 ID 체계를 바꿀 때마다 prefix 전체를 옮기는 식이라면 S3의 평면 네임스페이스는 그 워크로드와 성격이 안 맞는다. EFS의 POSIX rename 한 줄이 답이거나, 애초에 키 안에 변하지 않는 식별자(u-uuid-...)를 두는 설계가 답이다.
prefix 당 3,500 PUT / 5,500 GET, partition이 자동으로 나뉘는 곳
평면 네임스페이스가 만든 두 번째 운영 모형. Best practices design patterns: optimizing Amazon S3 performance 의 첫 줄을 그대로 인용한다.
Your application can achieve at least 3,500 PUT/COPY/POST/DELETE or 5,500 GET/HEAD requests per second per partitioned Amazon S3 prefix.
여기서 partitioned prefix 라는 단어가 무겁다. AWS가 공식적으로 약속하는 것은 한 줄, prefix 단위로 위 한도가 보장되고, 트래픽이 늘어나면 자동으로 점진적 스케일링이 일어난다는 것. 그 너머의 내부 메커니즘 (S3가 hot prefix를 둘로 쪼갠다는 식의 서술) 은 외부에서 관측되는 동작에 가깝지 공식 문서가 명시한 약속은 아니다. 사용자가 partition을 만들거나 지우는 API도 따로 없다. AWS 공식 문구는 그 자동 스케일링이 즉각적이지 않다는 한 줄로 마무리한다. "The scaling, in the case of both read and write operations, happens gradually and is not instantaneous".
한 시나리오. 한 게임 서비스에서 사용자 업로드 이미지를 uploads/{date}/{userId}/{filename} 식으로 키를 만든다고 해 보자. 어느 날 트래픽이 폭발해 uploads/2026-04-30/ prefix에 초당 4,000 PUT이 들어왔다. 처음에는 한도 3,500을 넘어 일부 호출이 503 Slow Down 으로 돌아오고, 시간이 지나며 S3의 자동 스케일링이 진행돼 같은 prefix가 더 높은 throughput을 흡수하기 시작한다. 사용자가 보는 결과는 처음 얼마간 일부 업로드가 retry 한두 번씩 더 걸리는 형태다. caller가 SDK의 자동 retry를 켜 두었다면 거의 못 느끼지만, 끄거나 retry 한도를 낮춰 두었다면 그 시간 동안 일부 사용자가 업로드 실패 화면을 본다.
Lambda 트리거 종류: API Gateway·SQS·EventBridge·S3에서 본 S3 EventNotification도 같은 partition 모형 위에서 돈다. PUT 한 건당 한 번씩 Lambda가 호출되니까, 같은 prefix에 PUT이 몰리면 Lambda 동시성 폭증이 따라온다. 자동 스케일링이 진행되는 동안에도 호출 속도는 그대로 진행한다. 그래서 prefix 설계는 단순한 폴더 이름이 아니라 운영 throughput의 단위다. AWS 권장은 트래픽이 몰릴 prefix에 자연스러운 분포(예: 사용자 ID 해시 prefix, 또는 ${userId}/${date}/ 처럼 사용자 ID가 키 첫 단계에)를 두는 것이다. 무엇보다, 2018년 7월 17일 이후로는 임의의 hex prefix를 키 앞에 강제로 붙이라는 옛 가이드를 AWS가 거뒀다. 같은 발표인 Increased Request Rate Performance 에서 그 권고를 함께 정리했다. 사람이 읽기 편한 키를 그대로 두어도 된다.
버킷 이름이 호스트 이름이 되는 곳
평면 네임스페이스의 또 한 형태. s3://my-app-uploads-2026/users/u-12345/avatar.png 라는 한 줄을 HTTPS URL로 풀면 Virtual-hosted–style URL 이 된다.
https://my-app-uploads-2026.s3.ap-northeast-2.amazonaws.com/users/u-12345/avatar.pnghttps://my-app-uploads-2026.s3.ap-northeast-2.amazonaws.com/users/u-12345/avatar.pngbucket-name.s3.region-code.amazonaws.com. 버킷 이름이 호스트 이름의 첫 단계다. 그래서 버킷 길이 3-63자, 소문자+숫자+점+하이픈, DNS 호환이라는 룰이 그저 정해진 글자수가 아니라 DNS 호스트 이름으로 쓸 수 있어야 한다는 한 줄 제약에서 자연스럽게 따라온다. 글로벌 unique 제약도 같은 곳에서 답이 나온다. 한 호스트 이름 공간이 partition 단위로 글로벌이라, 같은 이름 버킷을 두 사람이 가질 수 없다.
여기서 한 가지 함정. 버킷 이름에 . 을 넣으면 HTTPS virtual-hosted–style이 작동하지 않는다. AWS 공식 문구 그대로.
If you include periods in a bucket's name, you can't use virtual-host-style addressing over HTTPS, unless you perform your own certificate validation.
이유는 AWS 가 발급해 둔 wildcard 인증서가 *.s3.region.amazonaws.com 한 단계만 매칭한다는 데서 온다. my.app.s3.ap-northeast-2.amazonaws.com 같은 두 단계짜리 호스트 이름은 그 와일드카드 한 줄로 검증할 수 없다. 그래서 my.app 같은 점이 든 버킷 이름은 HTTP는 되지만 HTTPS에서 인증서 검증에 실패한다. 운영에서 거의 무조건 막아야 할 형태다. AWS 권장도 한 줄로 남아 있다. 문구로는 "we recommend that you avoid using periods in bucket names, except for buckets that are used only for static website hosting". 그래서 새 버킷 이름은 점 없이, 하이픈으로만 구분하는 게 default다.
path-style URL, https://s3.region.amazonaws.com/{bucket}/{key} 형식은 AWS가 원래 2019년 5월 8일 deprecation plan을 발표했다가, 2020년 9월 23일 update에서 일정 자체를 미뤘다. 현재 공식 문서는 "Amazon S3 supports both virtual-hosted–style and path-style URL access in all AWS Regions" 라고 한 줄을 명시하고, path-style은 미래에 중단 예정이라는 한 줄만 덧붙여 두었다. 그래서 두 형식 모두 작동은 하지만, 새 코드는 virtual-hosted-style을 default로 쓰는 게 안전한 길이다.
그래서 키 한 줄을 어떻게 짤까
여기까지가 콘솔이 폴더라고 부른 것의 실체에서 시작해 prefix 가 throughput, 인증서, rename 비용을 나누는 곳까지 닿는 한 흐름이다. 운영에서 키를 짤 때 따라가는 짧은 룰 셋.
첫째, 키 안에 변하지 않는 식별자를 가능하면 앞에 둔다. users/{uuid}/... 가 users/{username}/... 보다 안전한 이유는 username은 변할 수 있고 prefix rename이 N×Copy+Delete의 비용을 만든다는 데서 온다.
둘째, 대소문자와 슬래시 위치를 한 번 정하면 일관되게 간다. S3는 키를 byte-for-byte 보관하니까 Users/와 users/는 다른 prefix다. 한 운영 코드와 한 분석 코드가 두 개를 섞어 쓰면 LIST 결과가 절반씩 나뉜다.
셋째, 높은 throughput이 예상되는 prefix는 키 첫 단계에 자연스러운 분포를 둔다. 사용자 ID 해시, 랜덤 hex 한 글자, 날짜 자체. 무엇이든 한 prefix로 트래픽이 다 모이지 않게 한다. 단, 2018년 이후로는 임의의 prefix 를 강제로 붙이라는 옛 권고는 그대로 따라가지 않는다.
넷째, 버킷 이름은 점 없이, 하이픈으로만, 63자 안에서. 점이 든 버킷 이름은 HTTPS virtual-hosted–style을 깬다.
S3가 답이 아닌 곳도 같이 짚는다. 한 디렉토리 통째 rename이 일주일에 몇 번씩 일어나는 워크로드, POSIX rename 한 줄을 기대하는 레거시 앱, 같은 이름의 파일을 두 사용자가 동시에 같은 위치에 쓰면서 디렉토리 단위 잠금이 필요한 워크로드. 이런 곳에서는 EBS gp3 / io2 또는 EFS 가 답이다. "답이 아닌 곳은 분명하다"는 앞 글의 한 줄을 다시 적는다.
참고 자료
- Amazon S3 Console: Using folders. 0바이트 객체 + trailing 슬래시로 폴더 환상을 만드는 콘솔 동작의 출처
- ListObjectsV2 API Reference. Prefix·Delimiter·CommonPrefixes·MaxKeys·ContinuationToken 의 공식 정의
- Best practices design patterns: optimizing Amazon S3 performance. partitioned prefix 한 줄에 3,500 PUT/COPY/POST/DELETE, 5,500 GET/HEAD 의 출처
- Amazon S3 Announces Increased Request Rate Performance (2018-07-17). 임의 hex prefix 강제 권고가 폐기된 발표
- Bucket naming rules. 3-63자, 소문자·숫자·점·하이픈, 점이 든 버킷 이름의 HTTPS 인증서 와일드카드 충돌 출처
- Virtual hosting of buckets. virtual-hosted-style URL 정의와 path-style deprecation delay update (2020-09-23) 출처
- Amazon S3 Path Deprecation Plan: The Rest of the Story (2019-05-08). path-style 원래 deprecation plan 발표 글
- AWS CLI
mvreference. Copying, moving, and renaming objects.aws s3 mv가 copy + delete 페어로 동작한다는 출처











