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

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

Amazon S3에서 폴더가 어떻게 다뤄지는지에 대한 블로그의 번역입니다.
Clock Icon2023.09.21 14:32

안녕하세요 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바이트인 폴더의 유무입니다.

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.