Post

[AWS] AWS SDK v3의 모듈화 설계로 Lambda Cold Start 개선하기

SDK v2에서 v3로 마이그레이션하여 번들 크기를 줄이고 성능을 개선한 실전 경험

AWS SDK v2 vs v3

AWS SDK for JavaScript는 v3부터 완전히 재설계되었다. 가장 큰 변화는 모듈화다.

기본 차이점

항목SDK v2SDK v3
패키지 구조단일 패키지 (aws-sdk)모듈화 (@aws-sdk/*)
번들 크기50-70MB (전체)100-500KB (필요한 것만)
Lambda Layer70MB+5MB 이하
Cold Start1-2초300-500ms
TypeScript별도 타입 정의 필요네이티브 지원
Promise.promise() 호출 필요기본 Promise
Middleware제한적완전 지원

실전 비교: Collector Lambda

SDK v2 (Before)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ❌ 전체 SDK 로드 (50MB+)
const AWS = require('aws-sdk');

const s3 = new AWS.S3({ region: 'ap-northeast-2' });
const dynamodb = new AWS.DynamoDB({ region: 'ap-northeast-2' });
const cloudwatch = new AWS.CloudWatch({ region: 'ap-northeast-2' });
const sns = new AWS.SNS({ region: 'ap-northeast-2' });

// S3 업로드
const params = {
    Bucket: 'my-bucket',
    Key: 'data.json',
    Body: JSON.stringify(data)
};

s3.putObject(params).promise()  // 👈 .promise() 필수
    .then(result => console.log(result))
    .catch(err => console.error(err));

SDK v3 (After)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ✅ 필요한 클라이언트만 임포트 (각 ~100KB)
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
const { CloudWatchClient, PutMetricDataCommand } = require('@aws-sdk/client-cloudwatch');
const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');

// 클라이언트 초기화 (한 번만)
const s3Client = new S3Client({ region: 'ap-northeast-2' });
const dynamoDBClient = new DynamoDBClient({ region: 'ap-northeast-2' });
const cloudwatchClient = new CloudWatchClient({ region: 'ap-northeast-2' });
const snsClient = new SNSClient({ region: 'ap-northeast-2' });

// S3 업로드 (Command 패턴)
const command = new PutObjectCommand({
    Bucket: 'my-bucket',
    Key: 'data.json',
    Body: JSON.stringify(data),
    ContentType: 'application/json'
});

await s3Client.send(command);  // 👈 기본 Promise, .promise() 불필요

Command 패턴 (Command Pattern)

SDK v3는 Command 패턴을 도입했다. 각 API 호출이 독립적인 Command 객체가 된다.

장점

  1. 명확한 의도: 코드만 봐도 어떤 API를 호출하는지 명확
  2. 타입 안정성: TypeScript에서 자동 완성 지원
  3. 재사용 가능: Command 객체를 여러 번 재사용 가능
  4. Middleware 지원: Command에 미들웨어 추가 가능

예시: S3 파일 업로드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');

const s3Client = new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });

async function storeToS3(payload) {
    const now = new Date();
    const year = now.getUTCFullYear();
    const month = String(now.getUTCMonth() + 1).padStart(2, '0');
    const day = String(now.getUTCDate()).padStart(2, '0');

    // 데이터 타입 자동 감지
    let dataType = 'general';
    if (payload.performance && !payload.system_info) {
        dataType = 'performance';
    } else if (payload.system_info || payload.software || payload.patches) {
        dataType = 'data';
    }

    const s3Key = `processed/${payload.agent_metadata.group_name}/${dataType}/${year}/${month}/${day}/${payload.agent_metadata.agent_id}-${Date.now()}.json`;

    // Command 생성
    const command = new PutObjectCommand({
        Bucket: process.env.PROCESSED_BUCKET,
        Key: s3Key,
        Body: JSON.stringify(payload),
        ContentType: 'application/json'
    });

    // 전송
    await s3Client.send(command);
}

실전 사례: Processor Lambda

전체 코드 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
const { CloudWatchClient, PutMetricDataCommand } = require('@aws-sdk/client-cloudwatch');
const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');

// 클라이언트는 Lambda 컨테이너 재사용을 위해 전역으로 선언
const s3Client = new S3Client({
  region: process.env.AWS_REGION || 'us-east-1'
});
const dynamoDBClient = new DynamoDBClient({
  region: process.env.AWS_REGION || 'us-east-1'
});
const cloudwatchClient = new CloudWatchClient({
  region: process.env.AWS_REGION || 'us-east-1'
});
const snsClient = new SNSClient({
  region: process.env.AWS_REGION || 'us-east-1'
});

exports.handler = async (event, context) => {
  try {
    const processingPromises = event.Records.map(async (record) => {
      const payload = JSON.parse(record.body);

      // 1. 보안 위협 탐지
      if (payload.security_findings && Array.isArray(payload.security_findings) && payload.security_findings.length > 0) {
        for (const finding of payload.security_findings) {
          if (finding.severity === 'CRITICAL' || finding.severity === 'HIGH') {
            await sendAlert(finding, payload.agent_metadata);
          }
        }
      }

      // 2. S3 저장
      if (process.env.PROCESSED_BUCKET) {
        await storeToS3(payload);
      }

      // 3. DynamoDB 메타데이터 저장
      if (process.env.METADATA_TABLE) {
        await storeMetadata(payload);
      }

      // 4. CloudWatch 메트릭
      if (process.env.CLOUDWATCH_NAMESPACE) {
        await recordMetric(payload);
      }
    });

    await Promise.all(processingPromises);

    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'Processing completed',
        recordsProcessed: event.Records.length
      })
    };

  } catch (error) {
    console.error('Error processing SQS messages:', error);
    throw error;
  }
};

DynamoDB 저장

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function storeMetadata(payload) {
    const agentId = payload.agent_metadata?.agent_id || 'unknown';
    const timestamp = payload.timestamp || payload.agent_metadata?.timestamp || new Date().toISOString();
    const receivedAt = new Date().toISOString();
    const sourceIp = payload.source_ip || 'N/A';

    // DynamoDB Item 생성 - 빈 문자열은 제외
    const item = {
        agent_id: { S: String(agentId).trim() },
        timestamp: { S: String(timestamp).trim() },
        received_at: { S: String(receivedAt).trim() },
        source_ip: { S: String(sourceIp).trim() },
        data: { S: JSON.stringify(payload) }
    };

    const command = new PutItemCommand({
        TableName: process.env.METADATA_TABLE,
        Item: item
    });

    await dynamoDBClient.send(command);
}

CloudWatch 메트릭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
async function recordMetric(payload) {
    // 데이터 타입 자동 감지
    let dataType = 'general';
    if (payload.performance && !payload.system_info) {
        dataType = 'performance';
    } else if (payload.system_info || payload.software || payload.patches) {
        dataType = 'data';
    }

    const command = new PutMetricDataCommand({
        Namespace: process.env.CLOUDWATCH_NAMESPACE || 'ITSM/Agents',
        MetricData: [
            {
                MetricName: 'DataReceived',
                Value: 1,
                Unit: 'Count',
                Timestamp: new Date(),
                Dimensions: [
                    {
                        Name: 'AgentGroup',
                        Value: payload.agent_metadata.group_name
                    },
                    {
                        Name: 'AgentID',
                        Value: payload.agent_metadata.agent_id
                    },
                    {
                        Name: 'DataType',
                        Value: dataType  // 'data' 또는 'performance'
                    }
                ]
            }
        ]
    });

    await cloudwatchClient.send(command);
}

SNS 알림

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function sendAlert(finding, agentMetadata) {
    if (!process.env.ALERT_TOPIC_ARN) {
        console.log('ALERT:', {
            severity: finding.severity,
            finding: finding,
            agent: agentMetadata
        });
        return;
    }

    try {
        const command = new PublishCommand({
            TopicArn: process.env.ALERT_TOPIC_ARN,
            Message: JSON.stringify({
                severity: finding.severity,
                finding: finding,
                agent: agentMetadata,
                timestamp: new Date().toISOString()
            }),
            Subject: `Security Alert: ${finding.severity} - ${agentMetadata.agent_id}`
        });

        await snsClient.send(command);
    } catch (error) {
        console.error('Error sending SNS alert:', error);
    }
}

번들 크기 비교

SDK v2

1
2
3
4
5
{
  "dependencies": {
    "aws-sdk": "^2.1000.0"
  }
}
1
2
3
4
# Lambda 배포 패키지
Lambda function package size: 72.4 MB
Lambda Layer: Not used (too large)
Cold Start: 1,800ms

SDK v3

1
2
3
4
5
6
7
8
{
  "dependencies": {
    "@aws-sdk/client-s3": "^3.400.0",
    "@aws-sdk/client-dynamodb": "^3.400.0",
    "@aws-sdk/client-cloudwatch": "^3.400.0",
    "@aws-sdk/client-sns": "^3.400.0"
  }
}
1
2
3
4
# Lambda 배포 패키지
Lambda function package size: 4.8 MB
Lambda Layer: Can be used
Cold Start: 420ms

성능 향상

지표SDK v2SDK v3개선율
번들 크기72.4 MB4.8 MB93% 감소
Cold Start1,800ms420ms77% 단축
메모리 사용512 MB256 MB50% 감소
비용 (월간 1M 요청)$8.33$4.1750% 절감

마이그레이션 가이드

1. 패키지 교체

1
2
3
4
5
6
7
8
# v2 제거
npm uninstall aws-sdk

# v3 설치 (필요한 것만)
npm install @aws-sdk/client-s3
npm install @aws-sdk/client-dynamodb
npm install @aws-sdk/client-cloudwatch
npm install @aws-sdk/client-sns

2. 코드 변환 패턴

S3

1
2
3
4
5
6
7
8
9
// v2
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
await s3.putObject({ Bucket: 'bucket', Key: 'key', Body: 'data' }).promise();

// v3
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client({});
await s3.send(new PutObjectCommand({ Bucket: 'bucket', Key: 'key', Body: 'data' }));

DynamoDB

1
2
3
4
5
6
7
8
9
// v2
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB();
await dynamodb.putItem({ TableName: 'table', Item: { ... } }).promise();

// v3
const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
const dynamodb = new DynamoDBClient({});
await dynamodb.send(new PutItemCommand({ TableName: 'table', Item: { ... } }));

CloudWatch

1
2
3
4
5
6
7
8
9
// v2
const AWS = require('aws-sdk');
const cloudwatch = new AWS.CloudWatch();
await cloudwatch.putMetricData({ Namespace: 'NS', MetricData: [...] }).promise();

// v3
const { CloudWatchClient, PutMetricDataCommand } = require('@aws-sdk/client-cloudwatch');
const cloudwatch = new CloudWatchClient({});
await cloudwatch.send(new PutMetricDataCommand({ Namespace: 'NS', MetricData: [...] }));

Lambda 최적화 팁

1. 클라이언트를 전역으로 선언

1
2
3
4
5
6
// ✅ 좋은 예: Lambda 컨테이너 재사용
const s3Client = new S3Client({ region: 'ap-northeast-2' });

exports.handler = async (event) => {
    await s3Client.send(new PutObjectCommand({ ... }));
};
1
2
3
4
5
// ❌ 나쁜 예: 매번 새로 생성
exports.handler = async (event) => {
    const s3Client = new S3Client({ region: 'ap-northeast-2' });  // 비효율
    await s3Client.send(new PutObjectCommand({ ... }));
};

2. 리전 명시

1
2
3
4
5
// ✅ 좋은 예: 리전 명시
const s3Client = new S3Client({ region: 'ap-northeast-2' });

// ❌ 나쁜 예: 리전 누락 (환경 변수 의존)
const s3Client = new S3Client({});

리전을 명시하지 않으면 SDK가 자동으로 찾는 과정에서 시간이 소요된다.

3. 환경 변수 활용

1
2
3
const s3Client = new S3Client({
  region: process.env.AWS_REGION || 'us-east-1'
});

4. 병렬 처리

1
2
3
4
5
6
7
8
9
10
11
// ✅ 좋은 예: 병렬 처리
await Promise.all([
    storeToS3(payload),
    storeMetadata(payload),
    recordMetric(payload)
]);

// ❌ 나쁜 예: 순차 처리
await storeToS3(payload);
await storeMetadata(payload);
await recordMetric(payload);

TypeScript 지원

SDK v3는 TypeScript를 네이티브로 지원한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { S3Client, PutObjectCommand, PutObjectCommandInput } from '@aws-sdk/client-s3';

const s3Client = new S3Client({ region: 'ap-northeast-2' });

async function uploadFile(bucket: string, key: string, body: string): Promise<void> {
    const input: PutObjectCommandInput = {
        Bucket: bucket,
        Key: key,
        Body: body,
        ContentType: 'application/json'
    };

    const command = new PutObjectCommand(input);
    await s3Client.send(command);
}

자동 완성

1
2
3
4
5
6
7
8
9
const command = new PutObjectCommand({
    Bucket: 'my-bucket',
    Key: 'file.json',
    // 👇 IDE가 자동으로 사용 가능한 파라미터 제안
    Body: '...',
    ContentType: 'application/json',
    ACL: 'private',
    Metadata: { ... }
});

주의사항 (Gotchas)

1. .promise() 제거

1
2
3
4
5
6
// v2
await s3.putObject(params).promise();  // ✅

// v3
await s3Client.send(command).promise();  // ❌ .promise() 없음
await s3Client.send(command);            // ✅

2. 에러 타입 변경

1
2
3
4
5
6
7
8
9
10
11
12
13
// v2
catch (err) {
    if (err.code === 'NoSuchKey') { ... }
}

// v3
import { NoSuchKey } from '@aws-sdk/client-s3';

catch (err) {
    if (err instanceof NoSuchKey) { ... }
    // 또는
    if (err.name === 'NoSuchKey') { ... }
}

3. 응답 형태 동일

1
2
3
4
// v2, v3 모두 동일
const response = await s3.send(new GetObjectCommand({ ... }));
console.log(response.Body);  // Stream
console.log(response.ContentType);

실전 팁 (Best Practices)

1. 필요한 클라이언트만 임포트

1
2
3
4
5
// ✅ 좋은 예
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');

// ❌ 나쁜 예
const { S3Client, ...rest } = require('@aws-sdk/client-s3');  // 모든 것 임포트

2. Command 재사용

1
2
3
4
5
6
7
8
9
10
11
// 동일한 Command를 여러 번 재사용 가능
const command = new PutObjectCommand({
    Bucket: 'my-bucket',
    ContentType: 'application/json'
});

for (const data of dataList) {
    command.input.Key = `${data.id}.json`;
    command.input.Body = JSON.stringify(data);
    await s3Client.send(command);
}

3. 에러 핸들링

1
2
3
4
5
6
7
8
9
10
try {
    await s3Client.send(new PutObjectCommand({ ... }));
} catch (error) {
    console.error('S3 Error:', {
        name: error.name,
        message: error.message,
        statusCode: error.$metadata?.httpStatusCode
    });
    throw error;
}

마치며

AWS SDK v3는 모듈화 설계로 Lambda 성능을 크게 개선합니다.

필요한 클라이언트만 임포트하면 93% 번들 크기 감소, Command 패턴으로 명확하고 재사용 가능한 API 호출, Cold Start 77% 시간 단축(1.8초 → 420ms), TypeScript 네이티브 지원으로 자동 완성 및 타입 체크, 메모리 사용량 감소로 50% 비용 절감이 가능합니다.

Lambda 함수를 개발한다면, SDK v3로 마이그레이션하는 것이 성능과 비용 모두에서 유리합니다. 특히 Serverless Framework와 함께 사용하면 개발 경험이 훨씬 향상됩니다.

도움이 되셨길 바랍니다! 😀

This post is licensed under CC BY 4.0 by the author.