LabHub
배우기 러닝패스 코스

オブジェクトストレージとS3

delete markerと非現行バージョン

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

버저닝을 켜면 삭제는 지우는 것이 아니라 "지웠다는 표시(delete marker)" 를 하나 더 올리는 것이 됩니다. 원본은 그대로 남아 있고 요금도 계속 나옵니다.

概念マップ: 비동기・쓰는 코드가 어떤 보장 위에 서 있는지는 문서가 아니라 실측으로 확인합니다.・두 프로세스가 같은 키에 동시에 쓰는 문제・실수로 덮어쓰거나 지운 것을 되돌리는 문제

왜 이게 필요했나

S3 는 오랫동안 최종 일관성이었습니다. 객체를 덮어쓰고 바로 읽으면 옛 값이 올 수 있었고, 이 때문에 많은 애플리케이션이 재시도 로직을 넣었습니다. 2020년 12월부터 AWS S3 는 강한 읽기-쓰기 일관성을 제공합니다. PUT 직후의 GET 이 새 값을 봅니다.

다만 이 보장의 범위를 정확히 알아야 합니다.

연산 지금의 보장 주의
PUT 뒤 GET (새 키) 강한 일관성
PUT 뒤 GET (덮어쓰기) 강한 일관성
DELETE 뒤 GET 강한 일관성 버저닝을 켜면 404 뒤에 데이터가 남아 있다
LIST 강한 일관성 다른 구현은 대개 여기서 약하다
다중 리전 복제 비동기 복제본은 언제든 뒤처질 수 있다

"S3 는 이제 강한 일관성" 이라는 문장을 모든 S3 호환 스토리지에 그대로 적용하면 안 됩니다. MinIO 는 강한 일관성이지만, Ceph RGW 는 설정에 따라 다르고, SeaweedFS 는 filer 구성에 따라 목록 조회가 뒤처질 수 있습니다. 이 랩의 저장소도 마찬가지입니다. 쓰는 코드가 어떤 보장 위에 서 있는지는 문서가 아니라 실측으로 확인합니다.

조건부 요청으로 경쟁을 막는다

강한 일관성이 있어도 두 프로세스가 같은 키에 동시에 쓰는 문제 는 남습니다. 나중에 쓴 것이 이기므로 앞의 갱신이 조용히 사라집니다. S3 는 이것을 조건부 헤더로 막습니다.

# 없을 때만 만든다 — 있으면 412 Precondition Failed
s3.put_object(Bucket=b, Key=k, Body=data, IfNoneMatch="*")

# 내가 읽은 그 버전일 때만 덮어쓴다(낙관적 잠금)
head = s3.head_object(Bucket=b, Key=k)
s3.put_object(Bucket=b, Key=k, Body=new, IfMatch=head["ETag"])

IfMatch 가 412 를 내면 그 사이에 누가 바꾼 것입니다. 다시 읽어 병합하고 재시도 합니다. 이 패턴이 없으면 "동시에 저장 눌렀더니 한 사람 것만 남는" 사고가 납니다.

버저닝은 다른 문제를 푼다

버저닝이 푸는 것은 일관성이 아니라 실수로 덮어쓰거나 지운 것을 되돌리는 문제 입니다.

버저닝을 켜면 같은 키에 쓸 때마다 새 버전 ID 가 생깁니다. 최신 것이 현행 버전이고 나머지는 비현행 버전입니다. 옛 버전은 버전 ID 로 언제든 읽을 수 있습니다.

삭제가 흥미롭습니다. 버전 ID 없이 DELETE 하면 실제로 지워지지 않고 delete marker 라는 특수한 버전이 맨 위에 올라갑니다. 그래서 보통 GET 은 404 를 받지만, 데이터는 그 아래 그대로 있습니다.

키 report.csv 의 버전 스택 (위가 최신)

  delete marker   (v4)  ← 지금 GET 하면 404
  ────────────────────
  2026-09-05 판   (v3)  ← v4 를 지우면 이것이 다시 현행이 된다
  2026-09-04 판   (v2)
  2026-09-03 판   (v1)

delete marker 를 지우면 바로 아래 버전이 다시 현행이 됩니다. 이것이 실수 복구의 메커니즘입니다. 진짜로 지우려면 버전 ID 를 명시해 DELETE 해야 하고, 이 동작은 되돌릴 수 없습니다.

한 가지 더. 버저닝은 켤 수는 있어도 끌 수는 없습니다. Suspended 상태로만 바꿀 수 있고, 이미 쌓인 버전은 그대로 남습니다. Suspended 상태에서 쓴 객체는 버전 ID 가 null 인 특수 버전이 되어, 다음에 또 쓰면 그것을 덮어씁니다. 켜기 전에 수명주기 규칙을 먼저 정하는 이유입니다.

현장에서 만나는 모습

버저닝의 비용을 자주 놓칩니다. 로그 파일을 매일 같은 키에 덮어쓰는 코드에 버저닝을 켜면, 1년 뒤 그 키 하나에 365개 버전이 쌓여 있습니다. 목록에는 객체 하나로 보이는데 요금은 365배입니다. 그리고 "다 지웠는데 요금이 그대로" 인 상황도 delete marker 때문입니다.

숨은 비용이 하나 더 있습니다. 실패한 멀티파트 업로드입니다. 5GB 파일을 올리다 80% 에서 끊기면, 이미 올라간 조각들이 어디에도 보이지 않은 채 과금됩니다. 목록에도 안 나오고 버전 목록에도 안 나옵니다. list-multipart-uploads 로만 보입니다.

그래서 버저닝을 켤 때는 반드시 수명주기 규칙을 함께 겁니다. 세 규칙이 기본 세트 입니다.

{"Rules": [
  {"ID": "expire-noncurrent", "Status": "Enabled", "Filter": {},
   "NoncurrentVersionExpiration": {"NoncurrentDays": 30}},
  {"ID": "clean-delete-markers", "Status": "Enabled", "Filter": {},
   "Expiration": {"ExpiredObjectDeleteMarker": true}},
  {"ID": "abort-incomplete-mpu", "Status": "Enabled", "Filter": {},
   "AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7}}
]}

규칙은 하루 한 번 비동기로 도므로 즉시 반영되지 않습니다. 적용 다음 날 용량이 줄기 시작하면 정상입니다.

되돌릴 수 없는 삭제를 아예 막아야 하는 데이터라면 Object Lock 을 씁니다. GOVERNANCE 모드는 특별 권한이 있으면 풀 수 있고, COMPLIANCE 모드는 루트 계정도 기간 안에는 못 지웁니다. 감사 대상 로그에는 후자를 씁니다.

다음 실습에서 할 것

버저닝을 켜고 같은 키에 세 번 써서 버전을 쌓고, 옛 버전을 읽고, 삭제 후 delete marker 를 확인하고, 그것을 제거해 복구하고, 비현행 버전이 차지하는 용량을 계산한 뒤, 수명주기 규칙을 적용합니다. 마지막으로 조건부 PUT 이 412 를 내는 것을 직접 확인합니다.