본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 19. · 4 Views
S3 이벤트 트리거 완벽 가이드
AWS S3에 파일이 업로드되면 자동으로 Lambda가 실행되는 이벤트 트리거의 원리와 실전 활용법을 배웁니다. 이미지 리사이징, 파일 처리 자동화 등 실무에 바로 적용할 수 있는 패턴을 소개합니다.
목차
1. S3 이벤트 알림
어느 날 김개발 씨는 회사에서 이런 요구사항을 받았습니다. "사용자가 프로필 사진을 업로드하면 자동으로 썸네일을 만들어주세요." 김개발 씨는 고민에 빠졌습니다.
서버에서 계속 S3를 확인해야 하나? 그건 너무 비효율적인데...
S3 이벤트 알림은 S3 버킷에서 특정 이벤트가 발생했을 때 자동으로 알림을 보내주는 기능입니다. 마치 우체통에 편지가 도착하면 자동으로 벨이 울리는 것과 같습니다.
파일 업로드, 삭제, 복사 등의 작업이 일어나면 Lambda, SNS, SQS 같은 서비스에 즉시 알려줍니다.
다음 코드를 살펴봅시다.
// S3 버킷 이벤트 알림 설정 (AWS CLI)
aws s3api put-bucket-notification-configuration \
--bucket my-upload-bucket \
--notification-configuration '{
"LambdaFunctionConfigurations": [{
"LambdaFunctionArn": "arn:aws:lambda:region:account:function:processImage",
"Events": ["s3:ObjectCreated:*"],
"Filter": {
"Key": {
"FilterRules": [{
"Name": "prefix",
"Value": "uploads/"
}]
}
}
}]
}'
김개발 씨는 선배 개발자 박시니어 씨를 찾아갔습니다. "선배님, S3에 파일이 올라왔는지 계속 확인하는 코드를 짜야 하나요?" 박시니어 씨는 웃으며 대답했습니다.
"아니요, 그럴 필요 없어요. S3 이벤트 알림을 쓰면 됩니다." S3 이벤트 알림이란 정확히 무엇일까요?
쉽게 비유하자면, S3 이벤트 알림은 마치 아파트 택배 알림 서비스와 같습니다. 택배가 도착하면 문자 메시지가 자동으로 오듯이, S3에 파일이 올라오면 자동으로 다른 서비스에 알림이 갑니다.
우리가 계속 택배함을 확인할 필요가 없듯이, 서버가 계속 S3를 감시할 필요가 없는 것입니다. 이벤트 알림이 없던 시절에는 어땠을까요?
개발자들은 폴링(Polling) 방식을 사용해야 했습니다. 서버가 일정 시간마다 S3를 확인하고, 새로운 파일이 있는지 체크하는 방식이었습니다.
이 방법은 여러 문제가 있었습니다. 첫째, 불필요한 API 호출이 계속 발생해서 비용이 늘어났습니다.
둘째, 실시간성이 떨어졌습니다. 5분마다 확인한다면 최대 5분의 지연이 생기는 것입니다.
더 큰 문제는 서버 리소스 낭비였습니다. 파일이 올라오지 않아도 계속 확인 작업을 해야 했으니까요.
프로젝트가 커질수록 이런 폴링 작업이 쌓여서 서버 부하가 눈덩이처럼 불어났습니다. 바로 이런 문제를 해결하기 위해 S3 이벤트 알림이 등장했습니다.
이벤트 알림을 사용하면 **이벤트 기반 아키텍처(Event-Driven Architecture)**가 가능해집니다. 무언가 일어났을 때만 반응하면 되니까요.
또한 거의 실시간으로 처리가 가능합니다. 파일이 업로드되는 순간 Lambda 함수가 실행되니까요.
무엇보다 비용 절감이라는 큰 이점이 있습니다. 실제로 파일이 올라왔을 때만 Lambda가 실행되니 불필요한 비용이 사라집니다.
위의 코드를 살펴보겠습니다. AWS CLI를 통해 S3 버킷의 이벤트 알림을 설정하는 예제입니다.
LambdaFunctionConfigurations 부분이 핵심입니다. 여기서 어떤 Lambda 함수를 실행할지 지정합니다.
Events 필드에는 어떤 이벤트에 반응할지 정의합니다. 위 예제에서는 s3:ObjectCreated:*로 설정했으므로 모든 생성 이벤트(PUT, POST, COPY)에 반응합니다.
Filter 부분은 특정 경로의 파일만 처리하도록 필터링합니다. 여기서는 uploads/ 폴더에 올라온 파일만 처리합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 소셜 미디어 서비스를 개발한다고 가정해봅시다.
사용자가 이미지를 업로드하면 여러 작업이 필요합니다. 썸네일 생성, 워터마크 추가, 얼굴 인식, 부적절한 콘텐츠 필터링 등이 있죠.
S3 이벤트 알림을 활용하면 사용자가 이미지를 올리는 즉시 이 모든 작업이 자동으로 시작됩니다. 많은 스타트업과 대기업에서 이런 패턴을 적극적으로 사용하고 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 이벤트 중복 처리를 고려하지 않는 것입니다.
S3는 최소 한 번(at-least-once) 전달을 보장하지만, 드물게 같은 이벤트가 두 번 전달될 수 있습니다. 따라서 Lambda 함수는 **멱등성(Idempotency)**을 가지도록 설계해야 합니다.
같은 요청이 여러 번 와도 결과가 동일해야 한다는 뜻입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다. "아, 이렇게 간단하게 자동화할 수 있군요!" S3 이벤트 알림을 제대로 이해하면 서버리스 아키텍처를 효율적으로 구축할 수 있습니다.
폴링 방식의 비효율을 제거하고, 진정한 이벤트 기반 시스템을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 이벤트 필터를 잘 설정하면 불필요한 Lambda 실행을 줄일 수 있습니다
- Lambda 함수는 멱등성을 가지도록 설계하여 중복 이벤트에 대비하세요
- CloudWatch Logs로 이벤트 처리 상황을 모니터링하는 습관을 들이세요
2. 트리거 설정하기
김개발 씨는 이제 실제로 S3 트리거를 설정해보기로 했습니다. AWS 콘솔을 열고 S3 버킷 페이지에 들어갔지만, 어디서부터 시작해야 할지 막막했습니다.
박시니어 씨가 옆에서 말했습니다. "Lambda부터 만들고, 그 다음에 S3 트리거를 연결하면 됩니다."
S3 트리거 설정은 S3 버킷과 Lambda 함수를 연결하는 과정입니다. 마치 전구와 스위치를 전선으로 연결하는 것과 같습니다.
트리거를 설정하면 S3에서 특정 이벤트가 발생할 때 Lambda 함수가 자동으로 실행됩니다.
다음 코드를 살펴봅시다.
// Lambda 함수에 S3 트리거 추가 (AWS SAM template.yaml)
Resources:
ImageProcessFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs18.x
Events:
S3UploadEvent:
Type: S3
Properties:
Bucket: !Ref ImageBucket
Events: s3:ObjectCreated:*
Filter:
S3Key:
Rules:
- Name: prefix
Value: images/
- Name: suffix
Value: .jpg
박시니어 씨는 화면을 공유하며 설명을 시작했습니다. "트리거 설정은 크게 두 가지 방법이 있어요.
첫 번째는 AWS 콘솔에서 직접 설정하는 방법, 두 번째는 코드로 관리하는 Infrastructure as Code 방식입니다." 먼저 콘솔에서 설정하는 방법을 알아봅시다. Lambda 콘솔에 들어가서 함수를 선택하면 상단에 트리거 추가 버튼이 있습니다.
이 버튼을 클릭하고 S3를 선택하면 됩니다. 그러면 어떤 버킷에서, 어떤 이벤트 타입에 반응할지 설정할 수 있는 화면이 나타납니다.
마치 온라인 쇼핑몰에서 옵션을 선택하는 것처럼 직관적입니다. 하지만 실무에서는 Infrastructure as Code 방식을 많이 사용합니다.
왜 그럴까요? 콘솔에서 클릭으로 설정하면 편리하지만, 나중에 같은 설정을 다시 만들기 어렵습니다.
개발 환경, 스테이징 환경, 프로덕션 환경에 똑같이 설정하려면 매번 클릭을 반복해야 합니다. 실수하기도 쉽고, 누가 언제 뭘 바꿨는지 추적도 어렵습니다.
코드로 관리하면 이런 문제가 해결됩니다. 위 예제는 AWS SAM(Serverless Application Model) 템플릿입니다.
SAM은 AWS에서 제공하는 서버리스 애플리케이션 관리 도구입니다. YAML 파일 하나로 Lambda 함수와 S3 트리거를 함께 정의할 수 있습니다.
이 파일을 Git으로 관리하면 변경 이력도 추적할 수 있고, 여러 환경에 동일하게 배포할 수도 있습니다. 코드를 자세히 살펴보겠습니다.
Type: AWS::Serverless::Function은 Lambda 함수를 정의합니다. Events 섹션이 바로 트리거 설정 부분입니다.
Type: S3로 S3 이벤트를 트리거로 지정합니다. **Events: s3:ObjectCreated:***는 모든 파일 생성 이벤트에 반응하라는 의미입니다.
Filter 부분은 특히 중요합니다. prefix로 images/ 폴더만, suffix로 .jpg 확장자만 필터링합니다.
이렇게 하면 불필요한 Lambda 실행을 막을 수 있습니다. 실제 프로젝트에서는 어떻게 활용할까요?
예를 들어 문서 관리 시스템을 만든다고 가정해봅시다. 사용자가 PDF 파일을 업로드하면 자동으로 텍스트를 추출하고, OCR 처리를 하고, 검색 인덱스를 만들어야 합니다.
이 모든 작업을 각각의 Lambda 함수로 분리하고, S3 트리거로 연결할 수 있습니다. documents/ 폴더에 .pdf 파일이 올라오면 첫 번째 Lambda가 실행되고, 처리된 결과를 다른 S3 경로에 저장하면 다음 Lambda가 실행되는 식입니다.
주의해야 할 점이 있습니다. **순환 트리거(Circular Trigger)**를 조심해야 합니다.
Lambda가 같은 버킷에 파일을 쓰고, 그 파일이 다시 같은 Lambda를 트리거하면 무한 루프에 빠집니다. 이런 상황을 막으려면 입력 폴더와 출력 폴더를 명확히 분리하고, 필터를 정확하게 설정해야 합니다.
예를 들어 input/ 폴더의 파일만 트리거하고, 결과는 output/ 폴더에 저장하는 식입니다. 또 하나 중요한 것은 권한 설정입니다.
Lambda가 S3 이벤트를 받으려면 S3가 Lambda를 실행할 권한이 필요합니다. 콘솔에서 설정하면 자동으로 권한이 추가되지만, SAM이나 CloudFormation을 쓸 때는 이 권한이 자동으로 생성됩니다.
하지만 수동으로 AWS CLI를 쓴다면 aws lambda add-permission 명령으로 권한을 직접 추가해야 합니다. 김개발 씨는 SAM 템플릿을 작성하고 배포했습니다.
"와, 코드 한 번만 작성하면 어디서든 똑같이 배포할 수 있네요!" 박시니어 씨는 미소를 지었습니다. "맞아요, 이게 진짜 현대적인 방식입니다." 트리거 설정을 제대로 이해하면 복잡한 서버리스 워크플로우도 체계적으로 관리할 수 있습니다.
콘솔 클릭보다 코드로 관리하는 습관을 들이면, 나중에 규모가 커져도 문제없습니다.
실전 팁
💡 - 프로덕션 환경에서는 콘솔 클릭보다 SAM이나 Terraform 같은 IaC 도구를 사용하세요
- 필터를 정확히 설정해서 불필요한 Lambda 실행을 막으세요
- 순환 트리거를 방지하기 위해 입력/출력 폴더를 명확히 분리하세요
3. 이벤트 객체 구조
트리거 설정을 마친 김개발 씨는 Lambda 함수를 작성하기 시작했습니다. 그런데 문제가 생겼습니다.
"S3에서 어떤 파일이 업로드됐는지 어떻게 알 수 있죠?" 박시니어 씨는 Lambda 코드를 열어 보이며 말했습니다. "이벤트 객체 안에 모든 정보가 들어있어요."
S3 이벤트 객체는 S3에서 Lambda로 전달되는 데이터 구조입니다. 마치 택배 송장처럼 어떤 파일이, 어떤 버킷에, 언제 업로드되었는지 모든 정보를 담고 있습니다.
이 객체를 파싱하면 업로드된 파일의 정보를 얻을 수 있습니다.
다음 코드를 살펴봅시다.
// Lambda 함수에서 S3 이벤트 객체 파싱
exports.handler = async (event) => {
// S3는 여러 레코드를 배열로 전달할 수 있음
const records = event.Records;
for (const record of records) {
// 버킷 이름 추출
const bucketName = record.s3.bucket.name;
// 파일 키(경로) 추출 - URL 디코딩 필수
const objectKey = decodeURIComponent(
record.s3.object.key.replace(/\+/g, ' ')
);
// 파일 크기
const size = record.s3.object.size;
// 이벤트 타입 (Put, Post, Copy 등)
const eventName = record.eventName;
console.log(`처리할 파일: ${bucketName}/${objectKey} (${size} bytes)`);
}
};
김개발 씨는 코드를 보며 고개를 갸우뚱했습니다. "이벤트 객체가 이렇게 복잡하게 생겼어요?" 박시니어 씨는 웃으며 대답했습니다.
"처음엔 복잡해 보이지만, 구조를 알고 나면 아주 논리적이에요." S3 이벤트 객체란 정확히 무엇일까요? 쉽게 비유하자면, 이벤트 객체는 마치 택배 송장과 같습니다.
택배 송장에는 보내는 사람, 받는 사람, 물건 크기, 배송 시각 등이 적혀 있죠. S3 이벤트 객체도 마찬가지입니다.
어떤 버킷에, 어떤 파일이, 언제, 어떤 작업으로 생성되었는지 모든 메타데이터를 담고 있습니다. Lambda 함수는 이 정보를 읽어서 적절한 처리를 합니다.
이벤트 객체가 없다면 어떻게 될까요? Lambda는 눈을 가리고 일하는 것과 같습니다.
무언가 일어났다는 것만 알고, 정확히 무엇이 일어났는지 모릅니다. 어떤 파일이 업로드됐는지 알 수 없으니, S3 API를 직접 호출해서 최근 파일 목록을 가져와야 합니다.
비효율적일 뿐 아니라, 동시에 여러 파일이 올라오면 어떤 파일을 처리해야 할지도 불분명합니다. 바로 이런 문제를 해결하기 위해 S3는 풍부한 이벤트 객체를 제공합니다.
이벤트 객체를 사용하면 정확한 파일 식별이 가능해집니다. 버킷 이름과 파일 키를 바로 알 수 있으니까요.
또한 이벤트 타입 구분도 가능합니다. ObjectCreated:Put인지, ObjectCreated:Post인지, ObjectCreated:Copy인지 알 수 있습니다.
무엇보다 추가 API 호출이 필요 없다는 큰 이점이 있습니다. 모든 정보가 이미 전달되었으니까요.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 event.Records를 보면 배열 형태입니다.
S3는 여러 파일이 동시에 업로드되면 하나의 Lambda 호출에 여러 레코드를 묶어서 보낼 수 있습니다. 효율성을 위한 설계입니다.
record.s3.bucket.name에서 버킷 이름을 추출합니다. 이 부분이 핵심입니다.
다음으로 record.s3.object.key에서 파일 경로를 가져옵니다. 여기서 중요한 포인트가 있습니다.
S3 키는 URL 인코딩되어 있고, 공백은 플러스 기호로 대체됩니다. 따라서 decodeURIComponent와 replace(/\+/g, ' ')로 디코딩해야 합니다.
이 과정을 빼먹으면 파일 이름에 공백이나 한글이 있을 때 오류가 납니다. record.s3.object.size는 파일 크기를 바이트 단위로 알려줍니다.
이 정보로 큰 파일은 다른 방식으로 처리하거나, 크기 제한을 둘 수 있습니다. record.eventName은 정확히 어떤 이벤트인지 알려줍니다.
ObjectCreated:Put, ObjectCreated:Post, ObjectRemoved:Delete 같은 값입니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 동영상 스트리밍 서비스를 개발한다고 가정해봅시다. 사용자가 동영상을 업로드하면 여러 처리가 필요합니다.
먼저 파일 크기를 체크해서 너무 크면 거부합니다. 확장자를 보고 지원하는 포맷인지 확인합니다.
이벤트 타입을 보고 실제 업로드인지, 다른 버킷에서 복사된 것인지 구분합니다. 이 모든 판단을 이벤트 객체만으로 할 수 있습니다.
주의해야 할 점이 있습니다. 초보 개발자들이 흔히 하는 실수는 URL 디코딩을 빼먹는 것입니다.
파일 이름이 test.jpg처럼 단순하면 문제가 없지만, 내 사진.jpg나 résumé.pdf 같은 파일은 제대로 처리되지 않습니다. 반드시 디코딩을 해야 합니다.
또 하나는 레코드 배열을 반복 처리하지 않는 것입니다. 대부분은 레코드가 하나지만, 여러 개일 수도 있습니다.
for 루프로 모든 레코드를 처리하는 습관을 들여야 합니다. 또 다른 팁은 이벤트 구조를 문서화하는 것입니다.
AWS 공식 문서에 이벤트 객체 구조가 자세히 나와 있지만, 실제로 어떤 값이 오는지 보려면 CloudWatch Logs에 console.log(JSON.stringify(event, null, 2))를 찍어보는 것이 좋습니다. 한 번 보면 구조가 머릿속에 확실히 들어옵니다.
김개발 씨는 이벤트 객체를 파싱하는 코드를 작성했습니다. 테스트 파일을 업로드하고 CloudWatch Logs를 확인하니 정확히 파일 정보가 출력되었습니다.
"이제 어떤 파일인지 알 수 있으니, 본격적으로 처리할 수 있겠어요!" 박시니어 씨는 고개를 끄덕였습니다. 이벤트 객체 구조를 제대로 이해하면 Lambda 함수에서 정확한 파일 처리 로직을 작성할 수 있습니다.
URL 디코딩 같은 작은 디테일을 놓치지 않는 것이 중요합니다.
실전 팁
💡 - 파일 키는 반드시 URL 디코딩하고 공백 처리를 해야 합니다
- event.Records는 배열이므로 for 루프로 모든 레코드를 처리하세요
- CloudWatch Logs에 이벤트 전체를 찍어보면 구조를 이해하는 데 도움이 됩니다
4. 파일 처리 로직
이제 김개발 씨는 본격적으로 파일을 처리하는 코드를 작성할 차례입니다. "이벤트 객체에서 파일 정보를 얻었으니, 이제 S3에서 파일을 다운로드해야겠죠?" 박시니어 씨는 고개를 끄덕였습니다.
"맞아요, AWS SDK를 사용하면 간단합니다."
파일 처리 로직은 S3 이벤트를 받은 Lambda 함수가 실제로 파일을 읽고 가공하는 과정입니다. 마치 우편물을 받아서 봉투를 뜯고 내용을 읽는 것과 같습니다.
AWS SDK를 사용해 S3에서 파일을 가져오고, 처리한 후, 결과를 다시 저장합니다.
다음 코드를 살펴봅시다.
// S3에서 파일 가져와서 처리하기
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const s3Client = new S3Client({ region: 'ap-northeast-2' });
exports.handler = async (event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
// S3에서 파일 내용 가져오기
const getCommand = new GetObjectCommand({ Bucket: bucket, Key: key });
const { Body } = await s3Client.send(getCommand);
const fileContent = await Body.transformToByteArray();
// 파일 처리 로직 (예: 텍스트 파일 대문자 변환)
const processedContent = fileContent.toString('utf-8').toUpperCase();
// 처리된 파일을 다른 경로에 저장
const outputKey = key.replace('input/', 'output/');
const putCommand = new PutObjectCommand({
Bucket: bucket,
Key: outputKey,
Body: processedContent
});
await s3Client.send(putCommand);
console.log(`처리 완료: ${key} -> ${outputKey}`);
}
};
김개발 씨는 AWS SDK 문서를 펼쳐 놓고 코드를 작성하기 시작했습니다. 박시니어 씨가 옆에서 조언했습니다.
"SDK 버전 3을 쓰세요. 버전 2보다 훨씬 가볍고 빠릅니다." 파일 처리 로직이란 정확히 무엇일까요?
쉽게 비유하자면, 파일 처리는 마치 주방에서 요리하는 것과 같습니다. 냉장고(S3)에서 재료(파일)를 꺼내고, 손질하고(처리), 완성된 요리를 다시 냉장고에 넣습니다(저장).
Lambda 함수는 이 모든 과정을 자동으로 실행하는 로봇 셰프입니다. 옛날에는 파일 처리가 어떻게 이루어졌을까요?
서버에 파일을 직접 저장하고 처리했습니다. 문제는 서버 디스크 용량이 금방 차고, 여러 서버에서 같은 파일에 접근하기 어렵다는 점이었습니다.
백업도 복잡했습니다. 서버가 죽으면 파일도 함께 사라졌죠.
확장성도 제한적이었습니다. 트래픽이 늘어나면 서버를 늘려야 하는데, 파일 동기화 문제가 생겼습니다.
S3와 Lambda를 조합하면 이런 문제가 해결됩니다. 파일은 S3에 안전하게 저장되고, Lambda는 필요할 때만 실행되어 처리합니다.
무한 확장성이 가능합니다. 파일이 하나든 천 개든 Lambda가 자동으로 스케일링합니다.
내구성도 뛰어납니다. S3는 99.999999999%(11 nines)의 내구성을 보장합니다.
무엇보다 비용 효율이 좋습니다. 서버를 24시간 돌릴 필요 없이 실제 처리 시간만 과금됩니다.
위의 코드를 자세히 살펴보겠습니다. 먼저 AWS SDK v3를 import합니다.
v3는 모듈 방식이라 필요한 것만 가져올 수 있어 패키지 크기가 작습니다. S3Client를 생성할 때 region을 지정합니다.
Lambda는 환경변수에서 자동으로 region을 가져오지만, 명시적으로 쓰는 것이 좋습니다. GetObjectCommand로 S3에서 파일을 가져옵니다.
중요한 점은 Body가 스트림이라는 것입니다. transformToByteArray()로 전체 내용을 메모리에 로드합니다.
작은 파일은 괜찮지만, 큰 파일은 스트림으로 처리해야 메모리 문제가 없습니다. 파일 내용을 처리하는 부분은 예제에서는 단순히 대문자로 바꾸는 것이지만, 실제로는 이미지 리사이징, PDF 텍스트 추출, 비디오 트랜스코딩 등 다양한 작업이 가능합니다.
처리 로직이 길어지면 별도 함수로 분리하는 것이 좋습니다. PutObjectCommand로 처리된 파일을 S3에 다시 저장합니다.
여기서 핵심 포인트는 출력 경로를 입력 경로와 다르게 하는 것입니다. input/을 output/으로 바꾸면 순환 트리거를 방지할 수 있습니다.
실제 프로젝트에서는 어떻게 활용할까요? 예를 들어 이커머스 사이트를 운영한다고 가정해봅시다.
판매자가 상품 이미지를 업로드하면 여러 크기의 썸네일을 만들어야 합니다. 원본 이미지는 products/original/ 폴더에 저장되고, Lambda가 트리거되어 products/large/, products/medium/, products/small/ 폴더에 각각 다른 크기의 이미지를 저장합니다.
사용자는 디바이스에 맞는 크기의 이미지를 받아 빠르게 로딩할 수 있습니다. 주의해야 할 점이 있습니다.
메모리 제한을 고려해야 합니다. Lambda는 최대 10GB까지 메모리를 설정할 수 있지만, 큰 파일을 통째로 메모리에 올리면 문제가 생길 수 있습니다.
100MB 이상의 파일은 스트림 방식으로 처리하거나, ECS나 Fargate 같은 컨테이너 서비스를 고려해야 합니다. 또 하나는 타임아웃 설정입니다.
Lambda는 기본 타임아웃이 3초인데, 파일 처리는 시간이 오래 걸릴 수 있습니다. 이미지 리사이징이라면 30초, 비디오 처리라면 15분(최대값)으로 설정해야 합니다.
타임아웃을 너무 짧게 설정하면 처리 중간에 Lambda가 종료됩니다. 에러 처리도 중요합니다.
S3에서 파일을 못 가져오거나, 처리 중 오류가 나면 Lambda는 예외를 던집니다. try-catch로 감싸고, 오류가 나면 CloudWatch에 로그를 남기고, 실패한 파일 정보를 SQS나 DynamoDB에 기록해두면 나중에 재처리할 수 있습니다.
김개발 씨는 테스트 텍스트 파일을 input/ 폴더에 업로드했습니다. 몇 초 후 output/ 폴더에 대문자로 변환된 파일이 나타났습니다.
"와, 정말로 자동으로 처리되네요!" 박시니어 씨는 미소를 지었습니다. "이제 이미지 처리로 넘어가 볼까요?" 파일 처리 로직을 제대로 이해하면 복잡한 데이터 파이프라인도 구축할 수 있습니다.
메모리와 타임아웃, 에러 처리를 신경 쓰면서 안정적인 시스템을 만드세요.
실전 팁
💡 - 큰 파일은 스트림 방식으로 처리하여 메모리 사용을 줄이세요
- Lambda 타임아웃을 파일 처리 시간에 맞게 충분히 설정하세요
- 입력과 출력 경로를 명확히 분리하여 순환 트리거를 방지하세요
5. 이미지 리사이징 예제
김개발 씨가 실제로 구현해야 하는 기능은 프로필 사진 썸네일 생성이었습니다. "텍스트 파일은 쉬웠는데, 이미지는 어떻게 처리하죠?" 박시니어 씨는 코드를 열어 보이며 말했습니다.
"Sharp 라이브러리를 쓰면 간단해요."
이미지 리사이징은 원본 이미지를 여러 크기로 변환하는 작업입니다. 마치 사진관에서 여권 사진을 여러 장 인화하는 것과 같습니다.
Sharp 같은 이미지 처리 라이브러리를 Lambda에서 사용하면 자동으로 썸네일을 만들 수 있습니다.
다음 코드를 살펴봅시다.
// Sharp를 사용한 이미지 리사이징
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const sharp = require('sharp');
const s3Client = new S3Client({ region: 'ap-northeast-2' });
exports.handler = async (event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
// 원본 이미지 가져오기
const getCommand = new GetObjectCommand({ Bucket: bucket, Key: key });
const { Body } = await s3Client.send(getCommand);
const imageBuffer = await Body.transformToByteArray();
// 여러 크기로 리사이징
const sizes = [{ width: 200, suffix: 'small' }, { width: 500, suffix: 'medium' }];
for (const size of sizes) {
// Sharp로 리사이징 및 최적화
const resizedImage = await sharp(Buffer.from(imageBuffer))
.resize(size.width, null, { fit: 'inside' })
.jpeg({ quality: 80 })
.toBuffer();
// 리사이징된 이미지 저장
const outputKey = key.replace('original/', `${size.suffix}/`);
await s3Client.send(new PutObjectCommand({
Bucket: bucket,
Key: outputKey,
Body: resizedImage,
ContentType: 'image/jpeg'
}));
console.log(`생성 완료: ${outputKey} (${size.width}px)`);
}
}
};
김개발 씨는 Sharp 라이브러리 문서를 읽어보았습니다. "선배님, 이거 설치하면 되는 건가요?" 박시니어 씨는 고개를 끄덕였습니다.
"네, package.json에 추가하고 배포하면 Lambda Layer로 자동 포함됩니다." 이미지 리사이징이란 정확히 무엇일까요? 쉽게 비유하자면, 이미지 리사이징은 마치 사진을 여러 액자에 맞게 자르는 것과 같습니다.
벽에 걸 큰 액자, 책상에 놓을 중간 액자, 지갑에 넣을 작은 액자처럼 용도에 맞게 크기를 조절하는 것입니다. 웹 서비스에서도 PC용 큰 이미지, 태블릿용 중간 이미지, 모바일용 작은 이미지가 필요합니다.
이미지 리사이징 자동화가 없던 시절에는 어땠을까요? 디자이너나 개발자가 수동으로 이미지를 리사이징해야 했습니다.
포토샵을 열고, 이미지를 불러오고, 크기를 조절하고, 저장하는 과정을 반복했습니다. 사용자가 매일 수백 장의 이미지를 업로드하는 서비스라면 상상만 해도 끔찍합니다.
또한 원본 이미지를 그대로 웹에서 보여주면 로딩이 느려지고, 사용자 경험이 나빠집니다. S3 트리거와 Lambda를 조합하면 이 모든 것이 자동화됩니다.
사용자가 이미지를 업로드하는 순간, Lambda가 실행되어 여러 크기의 썸네일을 만듭니다. 완전 자동입니다.
사람이 개입할 필요가 없습니다. 즉시 처리됩니다.
업로드 후 몇 초 안에 모든 썸네일이 준비됩니다. 비용 절감도 가능합니다.
원본 대신 최적화된 이미지를 제공하면 CloudFront 트래픽 비용이 줄어듭니다. 위의 코드를 자세히 살펴보겠습니다.
Sharp는 Node.js에서 가장 빠른 이미지 처리 라이브러리입니다. libvips를 기반으로 하며, ImageMagick보다 훨씬 빠릅니다.
Lambda에서 사용하기에 최적입니다. require('sharp')로 가져오면 바로 사용할 수 있습니다.
이미지를 S3에서 가져온 후 Buffer.from(imageBuffer)로 Sharp에 전달합니다. resize() 메서드가 핵심입니다.
첫 번째 인자는 너비, 두 번째는 높이입니다. 높이를 null로 두면 비율을 유지하며 리사이징됩니다.
{ fit: 'inside' } 옵션은 이미지가 주어진 크기 안에 들어가도록 조절합니다. jpeg() 메서드로 포맷과 품질을 설정합니다.
quality: 80은 파일 크기와 화질의 균형이 좋은 값입니다. 100에 가까우면 화질은 좋지만 용량이 큽니다.
60 이하면 용량은 작지만 화질이 떨어집니다. 80이 일반적으로 권장되는 값입니다.
**toBuffer()**로 처리된 이미지를 Buffer로 반환합니다. 이 Buffer를 S3에 다시 업로드하면 됩니다.
ContentType을 image/jpeg로 명시하는 것이 중요합니다. 이걸 빼먹으면 브라우저가 이미지를 제대로 표시하지 못할 수 있습니다.
실제 프로젝트에서는 어떻게 활용할까요? 예를 들어 소셜 미디어 앱을 만든다고 가정해봅시다.
사용자가 사진을 올리면 여러 크기가 필요합니다. 피드에 보여줄 600px 이미지, 상세 페이지의 1200px 이미지, 알림에 쓸 100px 아이콘 등입니다.
위 코드의 sizes 배열에 크기를 추가하기만 하면 됩니다. Lambda가 자동으로 모든 크기를 만들어 줍니다.
또 다른 활용 사례는 워터마크 추가입니다. Sharp는 composite() 메서드로 이미지 합성이 가능합니다.
원본 이미지에 로고 이미지를 오버레이하면 워터마크가 추가됩니다. 저작권 보호가 필요한 서비스에 유용합니다.
주의해야 할 점이 있습니다. Lambda 패키지 크기를 고려해야 합니다.
Sharp는 네이티브 바이너리를 포함하므로 패키지가 꽤 큽니다. 다른 라이브러리와 함께 쓰면 50MB 제한을 초과할 수 있습니다.
이럴 때는 Lambda Layer를 사용하거나, Docker 이미지로 배포하는 방법을 고려해야 합니다. 또 하나는 메모리 설정입니다.
고해상도 이미지를 처리하려면 메모리가 많이 필요합니다. 4K 이미지는 1GB 이상 설정하는 것이 안전합니다.
메모리를 늘리면 CPU도 함께 늘어나서 처리 속도도 빨라집니다. 비용은 약간 올라가지만 실행 시간이 줄어들어 전체 비용은 비슷합니다.
에러 처리도 중요합니다. 손상된 이미지나 지원하지 않는 포맷이 올라올 수 있습니다.
try-catch로 감싸고, 오류가 나면 에러 로그를 남기고 건너뛰는 것이 좋습니다. 전체 배치가 실패하는 것보다 일부만 실패하는 게 낫습니다.
김개발 씨는 테스트 이미지를 업로드했습니다. CloudWatch Logs를 확인하니 small과 medium 폴더에 리사이징된 이미지가 생성되었다는 로그가 보였습니다.
S3 콘솔에서 확인해보니 정말로 두 크기의 썸네일이 만들어져 있었습니다. "완전 자동으로 되네요!" 박시니어 씨는 만족스러운 표정을 지었습니다.
이미지 리사이징을 마스터하면 사용자 경험을 크게 개선할 수 있습니다. 빠른 로딩, 반응형 디자인, 비용 절감을 동시에 얻을 수 있습니다.
실전 팁
💡 - Sharp는 빠르고 메모리 효율이 좋아 Lambda에 최적입니다
- 품질 80, fit: 'inside' 설정이 일반적으로 좋은 균형을 제공합니다
- Lambda 메모리는 처리할 이미지 크기에 맞게 충분히 설정하세요
6. 오류 처리
김개발 씨는 이미지 리사이징 기능을 프로덕션에 배포했습니다. 처음 며칠은 잘 작동했지만, 갑자기 CloudWatch에 오류 로그가 쌓이기 시작했습니다.
"선배님, 이미지 하나 때문에 전체 Lambda가 실패하고 있어요!" 박시니어 씨는 로그를 보며 말했습니다. "에러 처리가 빠져 있네요."
오류 처리는 Lambda 함수가 예외 상황에 대응하는 방법입니다. 마치 소방훈련처럼 문제가 생겼을 때 어떻게 대처할지 미리 정해두는 것입니다.
적절한 에러 처리가 없으면 작은 문제가 전체 시스템을 멈출 수 있습니다.
다음 코드를 살펴봅시다.
// 견고한 에러 처리를 포함한 Lambda 함수
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const sharp = require('sharp');
const s3Client = new S3Client({ region: 'ap-northeast-2' });
exports.handler = async (event) => {
const results = [];
for (const record of event.Records) {
try {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
console.log(`처리 시작: ${key}`);
// 지원하지 않는 파일 타입 필터링
if (!key.match(/\.(jpg|jpeg|png|webp)$/i)) {
console.warn(`지원하지 않는 파일 형식: ${key}`);
continue;
}
const getCommand = new GetObjectCommand({ Bucket: bucket, Key: key });
const { Body } = await s3Client.send(getCommand);
const imageBuffer = await Body.transformToByteArray();
// 이미지 처리 실패 시 catch
const resizedImage = await sharp(Buffer.from(imageBuffer))
.resize(200, null, { fit: 'inside' })
.jpeg({ quality: 80 })
.toBuffer();
const outputKey = key.replace('original/', 'thumbnails/');
await s3Client.send(new PutObjectCommand({
Bucket: bucket,
Key: outputKey,
Body: resizedImage,
ContentType: 'image/jpeg'
}));
results.push({ key, status: 'success' });
console.log(`처리 성공: ${key}`);
} catch (error) {
// 개별 파일 오류는 로그만 남기고 계속 진행
console.error(`처리 실패: ${record.s3.object.key}`, error);
results.push({ key: record.s3.object.key, status: 'failed', error: error.message });
}
}
// 전체 결과 반환
return {
statusCode: 200,
body: JSON.stringify({ processed: results.length, results })
};
};
박시니어 씨는 CloudWatch Logs를 가리키며 설명했습니다. "보세요, 여기 손상된 이미지 파일 하나가 올라왔어요.
근데 예외가 처리되지 않아서 Lambda 전체가 실패했습니다." 김개발 씨는 깨달았습니다. "아, 그래서 다른 정상 파일들도 처리가 안 된 거군요." 오류 처리란 정확히 무엇일까요?
쉽게 비유하자면, 오류 처리는 마치 공장 생산 라인의 불량품 처리와 같습니다. 컨베이어 벨트에서 불량품 하나가 나왔다고 전체 라인을 멈추면 안 됩니다.
불량품은 따로 빼내고, 나머지는 계속 처리해야 합니다. Lambda도 마찬가지입니다.
하나의 파일 처리가 실패해도 다른 파일들은 정상적으로 처리되어야 합니다. 에러 처리가 없던 코드는 어떤 문제가 있었을까요?
첫 번째 문제는 **연쇄 실패(Cascading Failure)**입니다. 손상된 이미지 파일 하나가 Lambda 전체를 멈춥니다.
그러면 같은 배치에 있던 다른 정상 파일들도 처리되지 않습니다. 두 번째 문제는 디버깅 어려움입니다.
어떤 파일이 문제인지, 왜 실패했는지 로그가 제대로 남지 않으면 원인 파악이 어렵습니다. 세 번째는 재시도 폭풍입니다.
Lambda가 실패하면 S3가 자동으로 재시도하는데, 근본 원인을 해결하지 않으면 같은 오류가 반복됩니다. 적절한 오류 처리를 하면 이런 문제가 해결됩니다.
**격리된 실패(Isolated Failure)**가 가능해집니다. try-catch로 개별 파일을 감싸면 하나가 실패해도 다른 파일 처리는 계속됩니다.
명확한 로깅도 가능합니다. 어떤 파일이 왜 실패했는지 정확히 기록됩니다.
**우아한 강등(Graceful Degradation)**도 구현할 수 있습니다. 일부 기능이 실패해도 서비스는 계속 작동합니다.
위의 코드를 한 줄씩 살펴보겠습니다. 가장 바깥쪽 for 루프 안에 try-catch 블록이 있습니다.
이것이 핵심입니다. 각 레코드(파일)마다 독립적으로 try-catch가 적용되어, 하나가 실패해도 다음 파일로 넘어갑니다.
파일 처리 전에 사전 검증을 합니다. 파일 확장자를 체크해서 이미지 파일이 아니면 경고만 남기고 건너뜁니다.
이렇게 하면 불필요한 처리를 막고, 예상 가능한 오류를 사전에 차단합니다. catch 블록에서 console.error로 상세한 오류 정보를 로깅합니다.
파일 키와 에러 메시지를 함께 남기면 나중에 문제 파악이 쉽습니다. CloudWatch Logs Insights로 실패한 파일 목록을 쿼리할 수도 있습니다.
results 배열에 성공과 실패를 모두 기록합니다. 이렇게 하면 Lambda 실행 결과를 추적할 수 있습니다.
나중에 실패한 파일만 따로 재처리하는 로직을 만들 수도 있습니다. 실제 프로젝트에서는 더 고급 패턴을 사용합니다.
**Dead Letter Queue(DLQ)**를 설정하면 반복적으로 실패하는 이벤트를 SQS나 SNS로 보낼 수 있습니다. 예를 들어 3번 재시도했는데도 실패하면 DLQ로 보내서, 나중에 사람이 직접 확인하게 할 수 있습니다.
CloudWatch Alarms를 설정하면 오류율이 급증할 때 알림을 받을 수 있습니다. 예를 들어 10분 동안 에러가 10개 이상 발생하면 Slack이나 이메일로 알림이 오도록 설정합니다.
문제를 빠르게 인지할 수 있습니다. Step Functions를 사용하면 복잡한 오류 처리 워크플로우를 구성할 수 있습니다.
예를 들어 리사이징 실패 시 원본 이미지를 다른 버킷으로 옮기고, 관리자에게 알림을 보내고, 데이터베이스에 실패 기록을 남기는 등의 작업을 자동화할 수 있습니다. 주의해야 할 점이 있습니다.
에러를 무조건 삼키지 마세요. catch 블록이 비어 있거나 로그만 남기고 끝내면, 문제가 있는지조차 모를 수 있습니다. 최소한 CloudWatch에 에러 로그를 남기고, 중요한 오류는 알림을 보내야 합니다.
재시도 로직도 신중하게 설계해야 합니다. 네트워크 일시적 오류는 재시도하면 성공할 수 있지만, 손상된 이미지 파일은 아무리 재시도해도 소용없습니다.
오류 타입을 구분해서, 재시도할 가치가 있는 오류만 재시도해야 합니다. Lambda 동시 실행 제한도 고려해야 합니다.
대량의 파일이 한꺼번에 올라오면 Lambda가 폭발적으로 실행됩니다. 계정 단위 동시 실행 제한(기본 1000개)을 초과하면 스로틀링이 발생합니다.
Reserved Concurrency를 설정해서 이 Lambda가 사용할 수 있는 최대 동시 실행 수를 제한하면, 다른 중요한 Lambda에 영향을 주지 않습니다. 김개발 씨는 에러 처리 코드를 추가하고 다시 배포했습니다.
이번에는 손상된 이미지가 올라와도 에러 로그만 남기고, 다른 이미지들은 정상적으로 처리되었습니다. "이제 안심이에요!" 박시니어 씨는 고개를 끄덕였습니다.
"에러 처리는 프로덕션 코드의 필수 요소입니다." 오류 처리를 제대로 이해하면 안정적이고 신뢰할 수 있는 시스템을 만들 수 있습니다. 작은 문제가 큰 장애로 번지지 않도록 방어선을 여러 겹 쌓는 것이 중요합니다.
실전 팁
💡 - 각 파일 처리를 독립적인 try-catch로 감싸서 하나의 실패가 전체에 영향을 주지 않게 하세요
- Dead Letter Queue를 설정해서 반복 실패하는 이벤트를 따로 관리하세요
- CloudWatch Alarms로 오류율을 모니터링하고 문제를 빠르게 감지하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
EFK 스택 로깅 완벽 가이드
마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.
Grafana 대시보드 완벽 가이드
실시간 모니터링의 핵심, Grafana 대시보드를 처음부터 끝까지 배워봅니다. Prometheus 연동부터 알람 설정까지, 초급 개발자도 쉽게 따라할 수 있는 실전 가이드입니다.
분산 추적 완벽 가이드
마이크로서비스 환경에서 요청의 전체 흐름을 추적하는 분산 추적 시스템의 핵심 개념을 배웁니다. Trace, Span, Trace ID 전파, 샘플링 전략까지 실무에 필요한 모든 것을 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.