실무환경에서 S3 에는 보통 수천개에서 많으면 수만개의 데이터가 쌓이게 됩니다. 특히 유저 관련 데이터의 경우 사용자가 많아질수록 프로필 이미지, 유저 업로드 이미지 등 많은 데이터가 쌓이게 되는데 이런 데이터는 유저의 탈퇴 혹은 정책에 따라 삭제가 필요한 경우가 있습니다. 이러한 대량의 객체 삭제가 필요한 경우 활용할 수 있는 세가지 방법을 소개하려 합니다.
1. AWS CLI를 사용한 방법
가장 직관적이고 빠르게 시작할 수 있는 방법입니다.
전체 버킷 내용 삭제:
aws s3 rm s3://bucket-name --recursive
특정 접두사(폴더) 삭제:
aws s3 rm s3://bucket-name/folder-name/ --recursive
패턴 매칭으로 삭제:
aws s3 rm s3://bucket-name --recursive --exclude "*" --include "*.log"
장점:
- 즉시 실행 가능
- 간단한 명령어
- 진행 상황 확인 가능
단점:
- DELETE 요청 비용 발생 (객체당 $0.0004)
- 대량 객체 시 시간 소요
- 네트워크 연결에 의존
2. AWS S3 Batch Operations
대용량 객체(수백만 개 이상)를 효율적으로 처리하는 엔터프라이즈급 솔루션입니다.
특징:
- S3 인벤토리 리스트나 CSV 파일 기반 작업
- 진행 상황 추적 및 보고서 제공
- 실패한 작업에 대한 상세 로그
- 병렬 처리로 높은 성능
- 작업 우선순위 설정 가능
사용 시나리오:
- 수백만 개 이상의 객체 처리
- 정확한 진행 상황 추적이 필요한 경우
- 실패 처리 및 재시도 로직이 중요한 경우
3. SDK를 통한 프로그래밍 방식
정교한 제어와 비즈니스 로직 통합이 가능한 방법입니다.
Python (Boto3) 예시:
import boto3
from concurrent.futures import ThreadPoolExecutor
s3 = boto3.client('s3')
def delete_objects_batch(bucket_name, prefix=''):
paginator = s3.get_paginator('list_objects_v2')
pages = paginator.paginate(Bucket=bucket_name, Prefix=prefix)
for page in pages:
if 'Contents' in page:
objects = [{'Key': obj['Key']} for obj in page['Contents']]
# 최대 1000개씩 배치 삭제
for i in range(0, len(objects), 1000):
batch = objects[i:i+1000]
response = s3.delete_objects(
Bucket=bucket_name,
Delete={'Objects': batch}
)
print(f"Deleted {len(batch)} objects")
# 병렬 처리를 통한 성능 향상
def parallel_delete(bucket_name, prefixes):
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(delete_objects_batch, bucket_name, prefix)
for prefix in prefixes]
for future in futures:
future.result()
장점:
- 세밀한 제어 가능
- 비즈니스 로직 통합 용이
- 조건부 삭제 구현 가능
단점:
- 개발 시간 필요
- API 호출 비용 발생
- 에러 처리 복잡성
S3 Lifecycle 정책: 가장 효율적인 해결책
이 글을 작성하고자 한 목적입니다. 아래 나열한 장점과 같이 자동화되고, 대규모 처리에도 사용자가 신경쓸 부분이 거의 없어 뛰어난 솔루션입니다.
Lifecycle 정책의 핵심 장점
- 비용 효율성: DELETE API 호출 비용 없음
- 완전 자동화: 수동 개입 불필요
- 대규모 처리: 객체 수에 제한 없음
- 유연한 조건: 다양한 필터링 옵션
기본 Lifecycle 정책 구성
날짜 기반 삭제:
{
"Rules": [
{
"ID": "DeleteOldLogs",
"Status": "Enabled",
"Filter": {
"Prefix": "logs/"
},
"Expiration": {
"Days": 30
}
},
{
"ID": "DeleteTempFiles",
"Status": "Enabled",
"Filter": {
"Prefix": "temp/"
},
"Expiration": {
"Days": 7
}
}
]
}
크기 기반 필터링:
{
"Rules": [
{
"ID": "DeleteSmallOldFiles",
"Status": "Enabled",
"Filter": {
"And": {
"Prefix": "cache/",
"ObjectSizeGreaterThan": 0,
"ObjectSizeLessThan": 1048576
}
},
"Expiration": {
"Days": 1
}
}
]
}
고급 태그 기반 삭제 전략
태그를 활용하면 더욱 정교한 객체 관리가 가능합니다.
태그 기반 Lifecycle 정책:
{
"Rules": [
{
"ID": "DeleteMarkedObjects",
"Status": "Enabled",
"Filter": {
"Tag": {
"Key": "delete",
"Value": "true"
}
},
"Expiration": {
"Days": 1
}
},
{
"ID": "DeleteExpiredContent",
"Status": "Enabled",
"Filter": {
"And": {
"Tags": [
{
"Key": "category",
"Value": "temporary"
},
{
"Key": "expires",
"Value": "yes"
}
]
}
},
"Expiration": {
"Days": 1
}
}
]
}
Lambda 함수를 통한 스마트 태깅 시스템
실제 운영 환경에서 사용되는 Lambda 함수 기반 자동 태깅 시스템을 구현해보겠습니다.
핵심 Lambda 함수 구현
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.handler = async (event) => {
const allTagPromises = [];
// 삭제할 객체 목록 가져오기 (DB, API, 또는 이벤트에서)
const objectsToDelete = await getObjectsToDelete(event);
console.log(`Processing ${objectsToDelete.length} objects for tagging`);
for (const row of objectsToDelete) {
const path = row.object_key || row.path;
const tagPromise = (async () => {
try {
// 메인 버킷에 태그 설정 시도
await s3.putObjectTagging({
Bucket: process.env.BUCKET_NAME,
Key: path,
Tagging: {
TagSet: [
{
Key: 'delete',
Value: 'true'
},
{
Key: 'tagged-date',
Value: new Date().toISOString()
},
{
Key: 'reason',
Value: row.delete_reason || 'automated-cleanup'
}
]
}
}).promise();
return { success: true, path, logIndex: row.log_index };
} catch (error) {
// 객체가 메인 버킷에 없는 경우 백업 버킷 시도
if (error.code === 'NoSuchKey') {
try {
await s3.putObjectTagging({
Bucket: process.env.BACKUP_BUCKET_NAME || "cdn.buldak.roundsquare.io",
Key: path,
Tagging: {
TagSet: [
{
Key: 'delete',
Value: 'true'
},
{
Key: 'tagged-date',
Value: new Date().toISOString()
},
{
Key: 'source',
Value: 'backup-bucket'
}
]
}
}).promise();
return { success: true, path, logIndex: row.log_index, source: 'backup' };
} catch (retryError) {
console.error(`Error tagging object ${path} in backup bucket:`, retryError);
return { success: false, path, logIndex: row.log_index, error: retryError };
}
}
console.error(`Error tagging object ${path}:`, error);
return { success: false, path, logIndex: row.log_index, error };
}
})();
allTagPromises.push(tagPromise);
}
// 모든 태깅 작업을 병렬로 실행
const results = await Promise.all(allTagPromises);
// 결과 분석
const successResults = results.filter(r => r.success);
const failureResults = results.filter(r => !r.success);
console.log(`Tagging completed: ${successResults.length} success, ${failureResults.length} failures`);
// CloudWatch 메트릭 전송
await sendMetricsToCloudWatch(successResults.length, failureResults.length);
// 실패한 항목들을 DLQ나 별도 테이블에 저장
if (failureResults.length > 0) {
await handleFailures(failureResults);
}
return {
statusCode: 200,
body: JSON.stringify({
success: successResults.length,
failures: failureResults.length,
details: {
mainBucket: successResults.filter(r => !r.source).length,
backupBucket: successResults.filter(r => r.source === 'backup').length
}
})
};
};
// 삭제할 객체 목록을 가져오는 함수
async function getObjectsToDelete(event) {
// 실제 환경에서는 다음과 같은 소스에서 데이터를 가져올 수 있습니다:
// 1. RDS/Aurora 데이터베이스 쿼리
// 2. DynamoDB 스캔
// 3. S3에서 CSV 파일 읽기
// 4. API Gateway를 통한 외부 요청
// 5. EventBridge 이벤트
if (event.Records) {
// SQS 메시지에서 처리할 경우
return event.Records.map(record => JSON.parse(record.body));
} else if (event.objects) {
// 직접 호출된 경우
return event.objects;
} else {
// 기본값 또는 다른 소스에서 가져오기
return [];
}
}
// CloudWatch 메트릭 전송
async function sendMetricsToCloudWatch(successCount, failureCount) {
const cloudwatch = new AWS.CloudWatch();
const params = {
Namespace: 'S3/ObjectTagging',
MetricData: [
{
MetricName: 'TaggedObjects',
Value: successCount,
Unit: 'Count',
Dimensions: [
{
Name: 'Environment',
Value: process.env.ENVIRONMENT || 'production'
}
]
},
{
MetricName: 'TaggingFailures',
Value: failureCount,
Unit: 'Count',
Dimensions: [
{
Name: 'Environment',
Value: process.env.ENVIRONMENT || 'production'
}
]
}
]
};
try {
await cloudwatch.putMetricData(params).promise();
} catch (error) {
console.error('Failed to send metrics to CloudWatch:', error);
}
}
// 실패한 항목 처리
async function handleFailures(failures) {
// DLQ로 전송하거나 별도 테이블에 저장
console.log('Failed items:', failures.map(f => ({ path: f.path, error: f.error?.message })));
// 실제 환경에서는 다음과 같은 처리를 할 수 있습니다:
// 1. SQS DLQ로 전송
// 2. DynamoDB 실패 테이블에 저장
// 3. SNS 알림 발송
// 4. CloudWatch 로그에 상세 기록
}
조건부 스마트 태깅
더 지능적인 태깅을 위한 고급 구현입니다.
// 조건부 태깅 함수
async function smartTagging(bucket, key, conditions = {}) {
try {
// 객체 메타데이터 및 태그 정보 가져오기
const [headResponse, tagsResponse] = await Promise.all([
s3.headObject({ Bucket: bucket, Key: key }).promise(),
s3.getObjectTagging({ Bucket: bucket, Key: key }).promise().catch(() => ({ TagSet: [] }))
]);
const lastModified = headResponse.LastModified;
const size = headResponse.ContentLength;
const contentType = headResponse.ContentType;
const existingTags = tagsResponse.TagSet;
const now = new Date();
const daysSinceModified = (now - lastModified) / (1000 * 60 * 60 * 24);
// 다양한 조건 검사
const shouldDelete = checkDeletionConditions({
key,
size,
daysSinceModified,
contentType,
existingTags,
conditions
});
if (shouldDelete.delete) {
const newTags = [
...existingTags.filter(tag => tag.Key !== 'delete'),
{
Key: 'delete',
Value: 'true'
},
{
Key: 'delete-reason',
Value: shouldDelete.reason
},
{
Key: 'tagged-timestamp',
Value: now.toISOString()
}
];
await s3.putObjectTagging({
Bucket: bucket,
Key: key,
Tagging: { TagSet: newTags }
}).promise();
return { tagged: true, reason: shouldDelete.reason };
}
return { tagged: false, reason: 'conditions-not-met' };
} catch (error) {
console.error(`Error in smart tagging for ${key}:`, error);
throw error;
}
}
function checkDeletionConditions({ key, size, daysSinceModified, contentType, existingTags, conditions }) {
// 이미 삭제 태그가 있는 경우
if (existingTags.some(tag => tag.Key === 'delete' && tag.Value === 'true')) {
return { delete: false, reason: 'already-tagged' };
}
// 임시 파일 조건
if (key.includes('/temp/') || key.endsWith('.tmp')) {
return { delete: true, reason: 'temporary-file' };
}
// 오래된 로그 파일
if (key.startsWith('logs/') && daysSinceModified > 30) {
return { delete: true, reason: 'old-log-file' };
}
// 큰 사이즈의 오래된 파일
if (size > 100 * 1024 * 1024 && daysSinceModified > 90) {
return { delete: true, reason: 'large-old-file' };
}
// 특정 콘텐츠 타입의 오래된 파일
if (contentType?.startsWith('image/') && daysSinceModified > 365) {
return { delete: true, reason: 'old-image-file' };
}
// 커스텀 조건
if (conditions.customCheck && conditions.customCheck(key, size, daysSinceModified)) {
return { delete: true, reason: 'custom-condition' };
}
return { delete: false, reason: 'no-matching-condition' };
}
실전 아키텍처 구성
1. 이벤트 기반 자동화 시스템
# CloudFormation 템플릿 예시
Resources:
ObjectTaggerFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: nodejs18.x
Handler: index.handler
Environment:
Variables:
BUCKET_NAME: !Ref MainBucket
BACKUP_BUCKET_NAME: !Ref BackupBucket
ENVIRONMENT: !Ref Environment
DailyTaggingSchedule:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: "cron(0 2 * * ? *)"
Targets:
- Arn: !GetAtt ObjectTaggerFunction.Arn
Id: DailyTaggingTarget
S3EventTrigger:
Type: AWS::S3::Bucket::NotificationConfiguration
Properties:
BucketName: !Ref MainBucket
LambdaConfigurations:
- Event: s3:ObjectCreated:*
Function: !GetAtt ObjectTaggerFunction.Arn
Filter:
S3Key:
Rules:
- Name: prefix
Value: temp/
2. 모니터링 및 알람 시스템
// CloudWatch 알람 설정
async function setupAlarms() {
const cloudwatch = new AWS.CloudWatch();
// 태깅 실패율 알람
await cloudwatch.putMetricAlarm({
AlarmName: 'S3-Tagging-Failure-Rate-High',
ComparisonOperator: 'GreaterThanThreshold',
EvaluationPeriods: 2,
MetricName: 'TaggingFailures',
Namespace: 'S3/ObjectTagging',
Period: 300,
Statistic: 'Sum',
Threshold: 100,
ActionsEnabled: true,
AlarmActions: [
'arn:aws:sns:region:account:alert-topic'
],
AlarmDescription: 'Alert when S3 object tagging failures exceed threshold',
Unit: 'Count'
}).promise();
}
복합 Lifecycle 정책 구성
실제 운영 환경에서 사용하는 복합 정책 예시입니다.
{
"Rules": [
{
"ID": "ImmediateDeletion",
"Status": "Enabled",
"Filter": {
"Tag": {
"Key": "delete-immediate",
"Value": "true"
}
},
"Expiration": {
"Days": 1
}
},
{
"ID": "ConditionalDeletion",
"Status": "Enabled",
"Filter": {
"And": {
"Prefix": "data/",
"Tags": [
{
"Key": "delete",
"Value": "true"
},
{
"Key": "environment",
"Value": "development"
}
]
}
},
"Expiration": {
"Days": 7
}
},
{
"ID": "TransitionAndDelete",
"Status": "Enabled",
"Filter": {
"Prefix": "archives/"
},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 90,
"StorageClass": "GLACIER"
}
],
"Expiration": {
"Days": 2555
}
}
]
}
성능 최적화 전략
1. 병렬 처리 최적화
// 청크 단위 병렬 처리
async function processInChunks(objects, chunkSize = 1000, concurrency = 10) {
const chunks = [];
for (let i = 0; i < objects.length; i += chunkSize) {
chunks.push(objects.slice(i, i + chunkSize));
}
const semaphore = new Semaphore(concurrency);
const results = await Promise.all(
chunks.map(chunk => semaphore.acquire(() => processChunk(chunk)))
);
return results.flat();
}
class Semaphore {
constructor(max) {
this.max = max;
this.count = 0;
this.queue = [];
}
async acquire(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.process();
});
}
process() {
if (this.count < this.max && this.queue.length > 0) {
this.count++;
const { fn, resolve, reject } = this.queue.shift();
fn().then(resolve, reject).finally(() => {
this.count--;
this.process();
});
}
}
}
2. 메모리 효율적인 처리
// 스트리밍 방식으로 대용량 객체 목록 처리
async function* streamObjects(bucket, prefix) {
let continuationToken;
do {
const params = {
Bucket: bucket,
Prefix: prefix,
MaxKeys: 1000,
ContinuationToken: continuationToken
};
const response = await s3.listObjectsV2(params).promise();
if (response.Contents) {
for (const object of response.Contents) {
yield object;
}
}
continuationToken = response.NextContinuationToken;
} while (continuationToken);
}
// 사용 예시
async function processLargeDataset(bucket, prefix) {
let processedCount = 0;
const batchSize = 100;
let batch = [];
for await (const object of streamObjects(bucket, prefix)) {
batch.push(object);
if (batch.length >= batchSize) {
await processBatch(batch);
processedCount += batch.length;
batch = [];
if (processedCount % 10000 === 0) {
console.log(`Processed ${processedCount} objects`);
}
}
}
// 남은 배치 처리
if (batch.length > 0) {
await processBatch(batch);
processedCount += batch.length;
}
console.log(`Total processed: ${processedCount} objects`);
}
비용 분석 및 최적화
방법별 비용 비교
방법 | API 비용 | 처리 시간 | 운영 복잡도 | 적합한 규모 |
---|---|---|---|---|
AWS CLI | 높음 ($0.0004/DELETE) | 중간 | 낮음 | 소규모 |
SDK/API | 높음 ($0.0004/DELETE) | 빠름 | 중간 | 중간규모 |
Batch Operations | 중간 ($1/job + $0.25/M objects) | 느림 | 중간 | 대규모 |
Lifecycle 정책 | 없음 | 매우 느림 (일단위) | 낮음 | 모든 규모 |
비용 최적화 팁
- 태깅 비용 고려: 객체당 $0.0004의 PUT 태깅 비용
- 배치 크기 최적화: 1000개 단위로 배치 삭제
- Lifecycle 정책 우선: 즉시 삭제가 불필요한 경우
- 조건 최적화: 불필요한 API 호출 최소화
주의사항 및 베스트 프랙티스
보안 고려사항
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectTagging",
"s3:PutObjectTagging",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket/*",
"arn:aws:s3:::my-bucket"
],
"Condition": {
"StringEquals": {
"s3:ExistingObjectTag/environment": "development"
}
}
}
]
}
운영 체크리스트
- 백업 확인: 중요한 데이터의 백업 상태 점검
- 테스트 환경: 프로덕션 적용 전 충분한 테스트
- 모니터링: CloudWatch 알람 및 로그 설정
- 롤백 계획: 문제 발생 시 복구 방안 수립
- 권한 검토: 최소 권한 원칙 적용
- 비용 추적: 예상 비용 계산 및 모니터링
문제 해결 가이드
일반적인 오류 및 해결책:
- NoSuchKey 오류: 객체가 이미 삭제되었거나 다른 버킷에 있음
- AccessDenied: IAM 권한 부족
- ThrottlingException: 요청 속도 제한 초과
- InvalidObjectState: 아카이브된 객체에 대한 작업 시도
결론
S3에서 대량의 객체를 삭제하는 작업은 단순해 보이지만, 규모와 요구사항에 따라 최적의 전략이 달라집니다. Lifecycle 정책을 중심으로 한 자동화 접근법은 비용 효율성과 운영 편의성을 모두 제공하는 최고의 솔루션입니다.
특히 Lambda 함수를 통한 스마트 태깅 시스템과 결합하면, 복잡한 비즈니스 로직을 반영한 정교한 객체 관리가 가능합니다. 이러한 시스템을 구축할 때는 점진적으로 접근하여 각 단계에서 충분한 테스트와 모니터링을 통해 안정성을 확보하는 것이 중요합니다.
대량 객체 삭제는 한 번 실행하면 되돌리기 어려운 작업이므로, 신중한 설계와 철저한 검증을 통해 안전하고 효율적인 시스템을 구축하시기 바랍니다.