Amazon S3의 “폴더”라는 환상을 파괴하고 그 실체를 알아보기

Amazon S3에서 폴더가 어떻게 다뤄지는지에 대한 블로그의 번역입니다.
2023.09.21

안녕하세요 DA사업본부 송영진입니다.

지난 번에 Amazon S3에서 폴더 이름 바꾸는 방법이 없나요? 라는 제목의 블로그를 작성 했었는데요, 오늘은 그 이해를 돕기 위해서 매우 도움이 되었던 都元ダイスケ님의 블로그를 번역한 내용으로 전달해보려고 합니다.

인삿말

잘 훈련된 애플 추종자, 미야모토입니다. Amazon S3 에 대한 자세한 설명은 필요하지 않겠지만, 요컨대 파일 스토리지입니다. HTTP 기반으로 파일을 업로드할 수 있고 다운로드할 수 있는 서비스입니다.

예전부터 데이터는 직렬화 된 형식으로 파일이라는 단위로 저장되고 관리되어 왔습니다. 로컬 머신내에서 파일을 관리하는 구조가 파일 시스템으로, 그 대부분에는 폴더라고 하는 계층 구조를 취급하는 구조가 갖추어져 있습니다.

Amazon S3는 Management Console을 통해 폴더를 만들고 그 안에 추가 폴더를 만들거나 파일을 저장할 수 있습니다. 그러나.

Amazon S3에는 사실 폴더라는 개념은 없다

입니다.

Amazon S3의 기본 기술은 단순한 KVS(Key-Value 유형 데이터스토어)일 뿐입니다. 예를 들면 아래와 같은 폴더(라고 우리가 인식하고 있는) 구조가 있다고 합니다. (본 엔트리에서는, bar.txt에는 bar , baz.txt에는 baz 라는 텍스트가 들어 있다고 간단하게 생각하기로 합니다.)

(루트)
 └ foo/
    └ bar.txt

하지만 위의 내용은 우리가 이렇게 인식하고 있을 뿐이지, S3내부에서는 단순히 아래와 같은 정보를 보관 유지하고 있는 것에 지나지 않습니다. S3에서는 기본적으로 / 에 특별한 의미는 없습니다.

Key (전체 경로) Value (파일 내용)
foo/bar.txt bar

Amazon S3에서 비어있는 폴더의 표현

여기서, S3가 이러한 구조로 정보를 보유하고 있다면 다음에 신경쓰이는 것은 비어있는 폴더를 어떻게 처리하느냐 입니다. 파일의 실체가 없으면 빈 폴더를 표현할 수 없습니다.

관리 콘솔에서 비어있는 폴더 만들기

예를 들어 관리 콘솔에서 새로 만든 버킷의 루트 바로 아래에 foo라는 빈 폴더를 만들 때 S3에서 파일 크기 0의 foo/라는 요소를 만듭니다 .

Key (전체 경로) Value (파일 내용)
foo/          (비어있음)

$ aws s3api list-objects --bucket cm-song-test
{
    "Contents": [
        {
            "Key": "foo/",
            "LastModified": "2023-09-21T09:14:11+00:00",
            "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
            "Size": 0,
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxxxxxxxx",
                "ID": "xxxxxxxxxxxxxxxxxxxxxxxx"
            }
        }
    ]
}

위와 같이 오브젝트는 있다고 나오지만 사이즈 0으로 됩니다.

관리 콘솔에서 기존 빈 폴더 안에 파일을 배치(케이스 1)

위의 빈 폴더에 bar.txt를 배치하면 다음과 같이 결과값이 변합니다.

Key (전체 경로) Value (파일 내용)
foo/          (비어있음)
foo/bar.txt          bar
$ aws s3api list-objects --bucket cm-song-test
{
    "Contents": [
        {
            "Key": "foo/",
            "LastModified": "2023-09-21T09:14:11+00:00",
            "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
            "Size": 0,
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxxxxxxxx",
                "ID": "xxxxxxxxxxxxxxxxxxxxxxxx"
            }
        },
        {
            "Key": "foo/bar.txt",
            "LastModified": "2023-09-21T09:20:38+00:00",
            "ETag": "\"c157a79031e1c40f85931829bc5fc552\"",
            "Size": 4,
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxxxxxxxx",
                "ID": "xxxxxxxxxxxxxxxxxxxxxxxx"
            }
        }
    ]
}

관리 콘솔로에서 보면 파일은 1개 밖에 없지만, 실제 S3 위에서는 이러한 2가지의 오브젝트가 관리되고 있습니다.

aws-cli를 사용하여 존재하지 않는 폴더에 파일을 직접 배치(케이스 2)

그렇다면 새로 만든 방금 버킷의 루트 디렉토리 바로 아래에 aws-cli를 사용하여 아래와 같은 명령으로 파일을 배치하면 어떻게 될까요?

$ echo bar > bar.txt
$ aws s3api put-object --bucket cm-song-test --key "foo/bar.txt" --body bar.txt

결과는 다음과 같습니다.

Key (전체 경로) Value (파일 내용)
foo/bar.txt          bar
$ aws s3api list-objects --bucket cm-song-test
{
    "Contents": [
        {
            "Key": "foo/bar.txt",
            "LastModified": "2023-09-21T09:31:11+00:00",
            "ETag": "\"c157a79031e1c40f85931829bc5fc552\"",
            "Size": 4,
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxxxxxxxx",
                "ID": "xxxxxxxxxxxxxxxxxxxxxxxx"
            }
        }
    ]
}

oh... 케이스 1과 2 사이에, 우리의 머리 속에서는 큰 차이는 없습니다만, S3적으로는 조금 다른 상황이 되고 있네요. 뭐, 그렇게 큰 문제가 되지 않는다고 생각합니다만.

덤으로 케이스 2의 상태에서 관리 콘솔에서 bar.txt를 삭제하면 foo/ 폴더가 새롭게 만들어집니다.

오브젝트의 리스트업

자, 여기서부터는 조금 복잡한 환경을 가정해두겠습니다. 이런 식으로.

(루트)
 ├ foo/
 │  ├ bar/
 │  │  └ qux.txt
 │  ├ baz/
 │  ├ quux/
 │  │  ├ corge.txt
 │  │  └ grault.txt
 │  └ garply.txt
 └ waldo.txt

S3에서는, 이렇게 표현할 수 있습니다. (일부러 폴더를 나타내는 빈 파일이 있거나 없거나 하는 상황을 만들어내고 있습니다.)

Key (전체 경로) Value (파일 내용)
foo/ (비어있음)
foo/bar/qux.txt qux
foo/baz/ (비어있음)
foo/quux/ (비어있음)
foo/quux/corge.txt corge
foo/quux/grault.txt grault
foo/garply.txt garply
waldo.txt waldo

모든 오브젝트 리스트업

이 상태에 대해, 모든 파일을 일람하는 명령어(API 액션)와 파라미터는, 지금까지 한 것과 같이, 이런 느낌입니다. (출력을 단순화하기 위해 jq 로 키만 출력하고 있습니다.)

$ aws s3api list-objects --bucket cm-song-test | jq ".Contents[].Key"
"foo/"
"foo/bar/qux.txt"
"foo/baz/"
"foo/garply.txt"
"foo/quux/"
"foo/quux/corge.txt"
"foo/quux/grault.txt"
"waldo.txt"

8건의 결과가 표시되었습니다.

prefix를 지정한 필터링

그런 다음 foo/ 폴더의 파일만 나열하려면 prefix 라는 옵션(매개 변수)을 부여합니다.

$ aws s3api list-objects --bucket cm-song-test --prefix "foo/" | jq ".Contents[].Key"                                                                                 
"foo/"
"foo/bar/qux.txt"
"foo/baz/"
"foo/garply.txt"
"foo/quux/"
"foo/quux/corge.txt"
"foo/quux/grault.txt"

이와 같이, prefix로 지정했을 경우, 그 문자열로 시작되는 키를 가지는 오브젝트 7건이 표시됩니다. 딱히 prefix 지정을 / 로 끝낼 필요는 없고, 이런 지정도 가능합니다.

$ aws s3api list-objects --bucket cm-song-test --prefix "foo/b"  | jq ".Contents[].Key"                                                                               
"foo/bar/qux.txt"
"foo/baz/"

특정 폴더 내부의 오브젝트만 표시하기 위해서는

파일 시스템에서 위와 같은 구조로 이루어져 있을 때, 현재 디렉토리가 foo인 경우, 그 때 리스트업하고 싶은 파일들은 "그 폴더 내부의 파일들만"이 일반적일 것입니다. 위와 같이 prefix로 좁히는 것만으로는, 예를 들어 foo/bar/ 폴더 안에 파일이 10000개 들어있었을 경우, 뒤쪽의 처리는 조금 번거로울 것 같고, 데이터의 취급으로서도 비효율적입니다. 여기서 list-objects의 결과에 몰래 존재하는 CommonPrefixes 라는 출력을 사용해서 해결 할 수 있습니다. 방금 전 7개의 결과가 표시된 명령에 --delimiter "/" 옵션을 추가하여 실행해 봅니다.

$ aws s3api list-objects --bucket cm-song-test --prefix "foo/" --delimiter "/"
{
    "Contents": [
        {
            "Key": "foo/",
            "LastModified": "2023-09-21T13:29:46+00:00",
            "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
            "Size": 0,
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxxxxxxxx",
                "ID": "xxxxxxxxxxxxxxxxxxxxxxxx"
            }
        },
        {
            "Key": "foo/garply.txt",
            "LastModified": "2023-09-21T13:32:15+00:00",
            "ETag": "\"cda2d9c3869a5fa5c4e09340afa8062b\"",
            "Size": 7,
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxxxxxxxx",
                "ID": "xxxxxxxxxxxxxxxxxxxxxxxx"
            }
        }
    ],
    "CommonPrefixes": [
        {
            "Prefix": "foo/bar/"
        },
        {
            "Prefix": "foo/baz/"
        },
        {
            "Prefix": "foo/quux/"
        }
    ]
}

무려 Contents에 출력된 것은, foo/ 폴더 그 자체와, 그 바로 내부에 있는 garply.txt에만 한정되었습니다. 그리고 CommonPrefixes로서, foo 바로 아래에 있는 3개의 폴더 bar, baz, quux가 나열되어 있습니다.

이와 같이 list-objects 액션에 대해 delimiter 파라미터로 경로의 단락 문자를 지정하면, 각 요소에 대해서 prefix 뒤에 최초로 나타나는 delimiter까지의 문자열로 그룹을 지어서………. 말로는 조금 설명하기 어려운데, 어쨌든 이렇게 "폴더로 인식 할 수 있는 것의 목록"을 CommonPrefixes에 출력할 수 있는 것 입니다.

Amazon S3 관리 콘솔

위의 내용처럼 S3는 단순한 Key-Value형 스토리지일 뿐입니다만, list-object 액션에 prefixdelimiter라는 파라미터를 추가함으로써, 특정의 폴더 내부의 파일들만을 나열하는 것 같은 행동을 실현할 수 있게 되어 있습니다. / 를 경로 구분 문자로 특별히 취급하는 것은 Amazon S3가 아니라 그 위에 있는 "관리 콘솔"입니다. Amazon S3 관리 콘솔은 이러한 메커니즘을 이용하여 순수한 S3로서는 개념이 존재하지 않는 '폴더'라는 환상을 우리에게 보여주고 있었습니다.

덤으로

전혀 실용성은 없습니다만, 아래와 같은 커멘드 결과에 대해서, 왜 이런 출력이 되었는지, 각자 고찰해 봐 주세요.

이 덤을 즐길 수 있는 분은, 이쪽 으로 부디.

$ aws s3api list-objects --bucket cm-song-test --prefix "fo" --delimiter "q"
{
    "Contents": [
        {
            "Key": "foo/",
            "LastModified": "2023-09-21T13:29:46+00:00",
            "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
            "Size": 0,
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxxxxxxxx",
                "ID": "xxxxxxxxxxxxxxxxxxxxxxxx"
            }
        },
        {
            "Key": "foo/baz/",
            "LastModified": "2023-09-21T13:30:16+00:00",
            "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
            "Size": 0,
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxxxxxxxx",
                "ID": "xxxxxxxxxxxxxxxxxxxxxxxx"
            }
        },
        {
            "Key": "foo/garply.txt",
            "LastModified": "2023-09-21T13:32:15+00:00",
            "ETag": "\"cda2d9c3869a5fa5c4e09340afa8062b\"",
            "Size": 7,
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxxxxxxxx",
                "ID": "xxxxxxxxxxxxxxxxxxxxxxxx"
            }
        }
    ],
    "CommonPrefixes": [
        {
            "Prefix": "foo/bar/q"
        },
        {
            "Prefix": "foo/q"
        }
    ]
}

마지막으로

어떠셨나요, S3는 진짜 파일 시스템이 아니라 Key-Value 스토리지라는 말 이해가 되셨나요? 저도 이 블로그를 읽고나서야 왜 어떤 때는 S3에서 리스트 가져올 때 폴더가 나오고 어떤 때는 안나오는지 이해가 가능했습니다. 여러분들께도 S3의 폴더에 대한 환상을 깨게 되는 계기가 되길 바라면서 이만 마치겠습니다. 감사합니다!

PS. 덤의 힌트는 0바이트인 폴더의 유무입니다.