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와 함께 사용하면 개발 경험이 훨씬 향상됩니다.

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