AWS SDK for C++: S3 멀티파트 업로드, DynamoDB CRUD, Lambda 호출, IAM 자격 증명

들어가며: “C++에서 AWS 서비스를 어떻게 연동하지?”

C++에서 AWS를 연동할 때 겪는 상황

C++로 서버·데스크톱 앱을 개발하다 보면, 클라우드 스토리지·DB·서버리스 함수를 연동해야 하는 순간이 옵니다. REST API를 직접 호출할 수도 있지만, AWS SDK for C++를 쓰면 인증·재시도·에러 처리를 SDK가 담당해 줍니다.

시나리오 1: 대용량 로그 파일을 S3에 업로드하다 메모리 부족

상황: 500MB 로그 파일을 PutObject로 한 번에 업로드하려다 메모리 할당 실패
문제: 단일 PutObject는 5GB 제한이 있지만, 전체를 메모리에 올리면 OOM 발생
결과: 멀티파트 업로드로 청크 단위(5MB~) 스트리밍 업로드

시나리오 2: DynamoDB 조회 시 “ValidationException: One or more parameter values were invalid”

상황: GetItem/Query 호출 시 파티션 키·정렬 키 형식이 테이블 스키마와 맞지 않음
문제: 문자열인데 숫자로 보냈거나, AttributeValue 타입 불일치
결과: SetS/SetN 등 올바른 AttributeValue 생성자 사용, 스키마 확인

시나리오 3: Lambda 호출 시 “ResourceNotFoundException” 또는 타임아웃

상황: C++ 앱에서 Lambda 함수를 Invoke했는데 404 또는 15초 후 타임아웃
문제: 함수명 오타, 리전 불일치, Lambda 실행 권한(IAM) 부족, 페이로드 크기 초과
결과: 함수명·리전·IAM 정책 확인, InvocationType::RequestResponse 시 타임아웃 설정

시나리오 4: S3 접근 시 “Access Denied” 또는 “SignatureDoesNotMatch”

상황: 로컬 개발 시 credentials 정상인데 EC2/ECS에서만 실패
문제: IAM Role 미할당, 환경 변수(AWS_ACCESS_KEY_ID 등) 누락, 리전·엔드포인트 오류
결과: EC2 Instance Profile 또는 ECS Task Role 설정, credentials 체인 확인

시나리오 5: 여러 AWS 서비스를 한 앱에서 사용할 때 클라이언트 관리

상황: S3·DynamoDB·Lambda를 동시에 쓰는데, 매번 새 클라이언트 생성하면 리소스 낭비
문제: 클라이언트는 스레드 세이프하지만, 불필요한 생성·소멸은 오버헤드
결과: 싱글톤 또는 의존성 주입으로 클라이언트 재사용, ClientConfiguration 공유
flowchart TB
    subgraph 문제[실무 문제]
        P1[대용량 S3 업로드] --> S1[멀티파트 업로드]
        P2[DynamoDB 타입 에러] --> S2[AttributeValue 올바른 사용]
        P3[Lambda 404/타임아웃] --> S3[리전·IAM·함수명 확인]
        P4[Access Denied] --> S4[IAM Role·Credentials]
        P5[클라이언트 관리] --> S5[재사용·싱글톤]
    end

이 글은 S3(PutObject, GetObject, 멀티파트 업로드, ListObjectsV2), DynamoDB(PutItem, GetItem, UpdateItem, Query), Lambda(동기·비동기 Invoke), 그리고 자격 증명 체인을 차례로 다룹니다. AWS SDK for C++는 다른 언어 SDK보다 초기화·종료 규칙과 스트림 기반 API가 까다로워서, 예제마다 실제로 크래시나 조용한 오류를 만드는 지점을 함께 짚습니다.

요구 환경: C++17 이상, AWS SDK for C++ 1.x


AWS SDK 빌드와 초기화

CMake로 AWS SDK 빌드

vcpkg 또는 시스템 패키지로 AWS SDK for C++를 설치합니다.

# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(aws_sdk_demo)
set(CMAKE_CXX_STANDARD 17)
# vcpkg 사용 시
find_package(aws-sdk-cpp REQUIRED COMPONENTS s3 dynamodb lambda core)
add_executable(aws_demo
    main.cpp
)
target_link_libraries(aws_demo
    PRIVATE
    aws-cpp-sdk-s3
    aws-cpp-sdk-dynamodb
    aws-cpp-sdk-lambda
    aws-cpp-sdk-core
)

vcpkg 설치 예시:

vcpkg install aws-sdk-cpp[s3,dynamodb,lambda]:x64-linux

SDK 초기화 및 ClientConfiguration

모든 AWS API 호출 전에 Aws::InitAPI를 호출하며, 종료 시 Aws::ShutdownAPI를 호출해야 합니다.

#include <aws/core/Aws.h>
#include <aws/core/client/ClientConfiguration.h>
#include <aws/s3/S3Client.h>
#include <iostream>
int main() {
    Aws::SDKOptions options;
    Aws::InitAPI(options);
    // 리전·엔드포인트·재시도 설정
    Aws::Client::ClientConfiguration config;
    config.region = "ap-northeast-2";  // 서울 리전
    config.connectTimeoutMs = 3000;
    config.requestTimeoutMs = 30000;
    config.maxConnections = 50;
    Aws::S3::S3Client s3Client(config);
    // ... API 호출 ...
    Aws::ShutdownAPI(options);
    return 0;
}

ClientConfiguration 주요 옵션:

  • region: 리전 (예: ap-northeast-2, us-east-1)
  • endpointOverride: 커스텀 엔드포인트 (로컬스택 등)
  • requestTimeoutMs: 요청 타임아웃
  • maxConnections: 연결 풀 크기

이 초기화 코드에는 AWS SDK for C++에서 가장 유명한 함정이 들어 있습니다. s3Client가 main의 지역 변수라서 Aws::ShutdownAPI(options)가 먼저 호출되고, 그 뒤 return 시점에 s3Client의 소멸자가 실행됩니다. 이미 종료된 SDK 내부 자원(HTTP 클라이언트 팩토리, 로깅, 암호 라이브러리)을 소멸자가 건드리면서 종료 시 세그폴트가 나거나 멈출 수 있습니다. 공식 예제가 항상 SDK 사용 코드를 중괄호 블록으로 감싸는 이유가 이것입니다.

Aws::SDKOptions options;
Aws::InitAPI(options);
{
    Aws::Client::ClientConfiguration config;
    config.region = "ap-northeast-2";
    Aws::S3::S3Client s3Client(config);
    // ... API 호출 ...
}   // 여기서 모든 클라이언트가 먼저 소멸
Aws::ShutdownAPI(options);

같은 이유로 뒤에서 볼 함수 내부 static 클라이언트(싱글톤)도 주의해야 합니다. 정적 객체는 main이 끝난 뒤 소멸하므로 항상 ShutdownAPI 이후에 파괴됩니다. 클라이언트를 전역 수명으로 두고 싶다면 std::unique_ptr로 보관하고 ShutdownAPI 직전에 명시적으로 reset()하는 편이 안전합니다.

ClientConfiguration에 region을 지정하지 않으면 SDK가 환경 변수, 프로파일, 그리고 EC2 메타데이터(IMDS) 순으로 리전을 찾습니다. EC2가 아닌 환경에서 IMDS 조회가 타임아웃될 때까지 기다리느라 클라이언트 생성이 몇 초씩 걸리는 경우가 있어, 로컬 개발이나 컨테이너에서는 리전을 명시하거나 AWS_EC2_METADATA_DISABLED=true를 설정해 두면 이런 지연을 피할 수 있습니다.


S3 파일 저장·다운로드

PutObject: 파일 업로드

#include <aws/core/Aws.h>
#include <aws/core/client/ClientConfiguration.h>
#include <aws/s3/S3Client.h>
#include <aws/s3/model/PutObjectRequest.h>
#include <fstream>
#include <iostream>
bool uploadFileToS3(const std::string& bucket, const std::string& key,
                   const std::string& filePath,
                   const Aws::Client::ClientConfiguration& config) {
    Aws::S3::S3Client client(config);
    auto inputStream = Aws::MakeShared<Aws::FStream>(
        "PutObjectStream", filePath.c_str(),
        std::ios_base::in | std::ios_base::binary);
    if (!*inputStream) {
        std::cerr << "파일 열기 실패: " << filePath << std::endl;
        return false;
    }
    Aws::S3::Model::PutObjectRequest request;
    request.SetBucket(bucket);
    request.SetKey(key);
    request.SetBody(inputStream);
    auto outcome = client.PutObject(request);
    if (!outcome.IsSuccess()) {
        std::cerr << "PutObject 실패: " << outcome.GetError().GetMessage()
                  << std::endl;
        return false;
    }
    std::cout << "업로드 완료: s3://" << bucket << "/" << key << std::endl;
    return true;
}

코드 설명:

  • Aws::FStream: 파일을 스트림으로 열어 메모리 효율적으로 전송
  • SetBucket/SetKey: 버킷명과 객체 키(경로)
  • PutObject는 동기 호출; 비동기는 PutObjectAsync 사용

이 예제의 uploadFileToS3처럼 함수를 호출할 때마다 S3Client를 새로 만드는 것은 예제를 짧게 하려는 선택일 뿐, 실무에서는 피해야 합니다. 클라이언트를 만들 때마다 HTTP 연결 풀, TLS 세션, 자격 증명 공급자가 새로 초기화되어, 파일을 수천 개 올리는 배치에서는 업로드 자체보다 클라이언트 생성 비용이 더 커집니다. SDK의 서비스 클라이언트는 여러 스레드에서 동시에 호출해도 안전하므로 하나를 만들어 공유하는 것이 기본입니다.

SetBody에 넘기는 스트림은 SDK가 요청 서명(본문 해시 계산)과 재시도 때 처음으로 되감아 다시 읽습니다. 그래서 한 번만 읽을 수 있는 파이프나 소켓 스트림을 그대로 넘기면 재시도 시 빈 본문이 전송될 수 있고, 파일 스트림을 ios_base::binary 없이 열면 Windows에서 줄바꿈 변환으로 내용이 달라져 업로드된 객체가 손상됩니다.

PutObject: 문자열(버퍼) 업로드

bool uploadStringToS3(const std::string& bucket, const std::string& key,
                     const std::string& content,
                     const Aws::Client::ClientConfiguration& config) {
    Aws::S3::S3Client client(config);
    auto stream = Aws::MakeShared<Aws::StringStream>("");
    *stream << content;
    Aws::S3::Model::PutObjectRequest request;
    request.SetBucket(bucket);
    request.SetKey(key);
    request.SetBody(stream);
    auto outcome = client.PutObject(request);
    return outcome.IsSuccess();
}

GetObject: 파일 다운로드

#include <aws/s3/model/GetObjectRequest.h>
bool downloadFromS3(const std::string& bucket, const std::string& key,
                   const std::string& localPath,
                   const Aws::Client::ClientConfiguration& config) {
    Aws::S3::S3Client client(config);
    Aws::S3::Model::GetObjectRequest request;
    request.SetBucket(bucket);
    request.SetKey(key);
    auto outcome = client.GetObject(request);
    if (!outcome.IsSuccess()) {
        std::cerr << "GetObject 실패: " << outcome.GetError().GetMessage()
                  << std::endl;
        return false;
    }
    auto& result = outcome.GetResult();
    auto& body = result.GetBody();
    std::ofstream ofs(localPath, std::ios::binary);
    ofs << body.rdbuf();
    ofs.close();
    std::cout << "다운로드 완료: " << localPath << std::endl;
    return true;
}

멀티파트 업로드 (5MB 이상 권장)

5MB 이상 파일은 멀티파트 업로드로 청크 단위 전송하면 메모리·재시도에 유리합니다.

#include <aws/s3/model/CreateMultipartUploadRequest.h>
#include <aws/s3/model/UploadPartRequest.h>
#include <aws/s3/model/CompleteMultipartUploadRequest.h>
#include <aws/s3/model/AbortMultipartUploadRequest.h>
#include <fstream>
#include <vector>
struct PartETag {
    int partNumber;
    Aws::String eTag;
};
bool multipartUpload(const std::string& bucket, const std::string& key,
                    const std::string& filePath,
                    const Aws::Client::ClientConfiguration& config,
                    size_t partSize = 5 * 1024 * 1024) {  // 5MB
    Aws::S3::S3Client client(config);
    std::ifstream file(filePath, std::ios::binary | std::ios::ate);
    if (!file) return false;
    size_t fileSize = file.tellg();
    file.seekg(0);
    // 1. 멀티파트 업로드 시작
    Aws::S3::Model::CreateMultipartUploadRequest createReq;
    createReq.SetBucket(bucket);
    createReq.SetKey(key);
    auto createOutcome = client.CreateMultipartUpload(createReq);
    if (!createOutcome.IsSuccess()) {
        std::cerr << "CreateMultipartUpload 실패" << std::endl;
        return false;
    }
    auto uploadId = createOutcome.GetResult().GetUploadId();
    // 2. 각 파트 업로드
    std::vector<PartETag> parts;
    int partNumber = 1;
    std::vector<char> buffer(partSize);
    while (file.read(buffer.data(), partSize) || file.gcount() > 0) {
        size_t bytesRead = file.gcount();
        if (bytesRead == 0) break;
        auto partStream = Aws::MakeShared<Aws::StringStream>("");
        partStream->write(buffer.data(), bytesRead);
        partStream->seekg(0);
        Aws::S3::Model::UploadPartRequest partReq;
        partReq.SetBucket(bucket);
        partReq.SetKey(key);
        partReq.SetUploadId(uploadId);
        partReq.SetPartNumber(partNumber);
        partReq.SetBody(partStream);
        auto partOutcome = client.UploadPart(partReq);
        if (!partOutcome.IsSuccess()) {
            client.AbortMultipartUpload(
                Aws::S3::Model::AbortMultipartUploadRequest()
                    .WithBucket(bucket).WithKey(key).WithUploadId(uploadId));
            return false;
        }
        parts.push_back({partNumber, partOutcome.GetResult().GetETag()});
        partNumber++;
    }
    // 3. 멀티파트 업로드 완료
    Aws::S3::Model::CompletedMultipartUpload completed;
    for (const auto& p : parts) {
        Aws::S3::Model::CompletedPart cp;
        cp.SetETag(p.eTag);
        cp.SetPartNumber(p.partNumber);
        completed.AddParts(cp);
    }
    Aws::S3::Model::CompleteMultipartUploadRequest completeReq;
    completeReq.SetBucket(bucket);
    completeReq.SetKey(key);
    completeReq.SetUploadId(uploadId);
    completeReq.WithMultipartUpload(completed);
    auto completeOutcome = client.CompleteMultipartUpload(completeReq);
    if (!completeOutcome.IsSuccess()) {
        std::cerr << "CompleteMultipartUpload 실패" << std::endl;
        return false;
    }
    std::cout << "멀티파트 업로드 완료: " << key << std::endl;
    return true;
}

멀티파트 업로드 포인트:

  • 최소 파트 크기 5MB (마지막 파트 제외)
  • 최대 10,000 파트
  • 실패 시 AbortMultipartUpload로 정리

파트 크기를 5MB로 고정하면 10,000파트 제한 때문에 약 48.8GB가 넘는 파일은 올릴 수 없습니다(5MB × 10,000). 파일 크기를 먼저 확인해 max(5MB, fileSize / 10000을 올림한 값)으로 파트 크기를 정하는 것이 안전하고, 네트워크가 안정적이라면 8~16MB 정도로 키우는 편이 요청 수와 오버헤드를 줄입니다.

AbortMultipartUpload를 호출하지 못한 채 프로세스가 죽으면(크래시, 강제 종료), 이미 올린 파트는 보이지 않는 상태로 버킷에 남아 계속 요금이 나갑니다. 콘솔의 객체 목록에도 나오지 않아 모르고 지나치기 쉽습니다. 버킷에 “완료되지 않은 멀티파트 업로드를 N일 후 삭제”(AbortIncompleteMultipartUpload) 수명 주기 규칙을 걸어 두는 것이 사실상 필수입니다. 또 이 코드는 파트를 하나씩 순차로 올리므로 대역폭을 다 쓰지 못합니다. 직접 병렬화하기보다 뒤에서 소개하는 TransferManager가 파트 크기 계산, 병렬 업로드, 재시도, 중단 시 정리를 한꺼번에 처리하므로, 특별한 이유가 없다면 그쪽을 먼저 검토하는 것이 좋습니다.

ListObjectsV2: 객체 목록 조회

ListObjectsV2는 한 번에 최대 1,000개 키만 돌려주므로 아래처럼 ContinuationToken으로 반복해야 합니다. 아래 코드는 중간에 요청이 실패하면 break로 빠져나와 그때까지 모은 일부 목록을 정상 결과처럼 반환한다는 점을 주의하세요. 삭제·동기화처럼 전체 목록이 정확해야 하는 작업이라면 실패를 호출자에게 알려야 합니다.

#include <aws/s3/model/ListObjectsV2Request.h>
std::vector<Aws::String> listS3Objects(const std::string& bucket,
                                       const std::string& prefix,
                                       const Aws::Client::ClientConfiguration& config) {
    Aws::S3::S3Client client(config);
    std::vector<Aws::String> keys;
    Aws::String continuationToken;
    do {
        Aws::S3::Model::ListObjectsV2Request request;
        request.SetBucket(bucket);
        if (!prefix.empty()) request.SetPrefix(prefix);
        if (!continuationToken.empty()) request.SetContinuationToken(continuationToken);
        auto outcome = client.ListObjectsV2(request);
        if (!outcome.IsSuccess()) break;
        for (const auto& obj : outcome.GetResult().GetContents()) {
            keys.push_back(obj.GetKey());
        }
        continuationToken = outcome.GetResult().GetNextContinuationToken();
    } while (!continuationToken.empty());
    return keys;
}

DynamoDB NoSQL 연동

PutItem: 아이템 저장

#include <aws/dynamodb/DynamoDBClient.h>
#include <aws/dynamodb/model/PutItemRequest.h>
#include <aws/dynamodb/model/AttributeValue.h>
bool putDynamoItem(const std::string& tableName,
                  const std::string& pkName, const std::string& pkValue,
                  const std::string& skName, const std::string& skValue,
                  const Aws::Client::ClientConfiguration& config) {
    Aws::DynamoDB::DynamoDBClient client(config);
    Aws::DynamoDB::Model::PutItemRequest request;
    request.SetTableName(tableName);
    request.AddItem(pkName, Aws::DynamoDB::Model::AttributeValue().SetS(pkValue));
    request.AddItem(skName, Aws::DynamoDB::Model::AttributeValue().SetS(skValue));
    // 추가 속성
    request.AddItem("createdAt", Aws::DynamoDB::Model::AttributeValue().SetS(
        std::to_string(std::chrono::system_clock::now().time_since_epoch().count())));
    auto outcome = client.PutItem(request);
    if (!outcome.IsSuccess()) {
        std::cerr << "PutItem 실패: " << outcome.GetError().GetMessage() << std::endl;
        return false;
    }
    return true;
}

createdAt에 system_clock::now().time_since_epoch().count()를 문자열로 넣은 부분은 개선할 여지가 있습니다. count()의 단위는 구현마다 다르고(libstdc++는 나노초, MSVC는 100나노초 단위), 문자열(S)로 저장하면 자릿수가 다를 때 정렬·범위 조건이 기대대로 동작하지 않습니다. duration_cast<seconds>로 에포크 초를 구해 숫자(N)로 저장하거나, ISO 8601 문자열로 통일하는 편이 쿼리하기 쉽습니다. 에포크 초 숫자 속성은 DynamoDB TTL 기능에도 그대로 쓸 수 있습니다.

PutItem은 같은 키의 아이템이 있으면 통째로 덮어씁니다. 덮어쓰기를 막으려면 SetConditionExpression("attribute_not_exists(#pk)")를 걸고, 실패 시 ConditionalCheckFailedException을 처리합니다.

AttributeValue 타입:

  • SetS(s): 문자열
  • SetN(n): 숫자 (문자열로 전달, 예: "123")
  • SetB(data): 바이너리
  • SetBool(b): 불리언
  • SetSS(set): 문자열 집합
  • SetM(map): 맵(중첩 문서)

GetItem: 단일 아이템 조회

#include <aws/dynamodb/model/GetItemRequest.h>
std::optional<Aws::Map<Aws::String, Aws::DynamoDB::Model::AttributeValue>>
getDynamoItem(const std::string& tableName,
              const std::string& pkName, const std::string& pkValue,
              const std::string& skName, const std::string& skValue,
              const Aws::Client::ClientConfiguration& config) {
    Aws::DynamoDB::DynamoDBClient client(config);
    Aws::DynamoDB::Model::GetItemRequest request;
    request.SetTableName(tableName);
    request.AddKey(pkName, Aws::DynamoDB::Model::AttributeValue().SetS(pkValue));
    request.AddKey(skName, Aws::DynamoDB::Model::AttributeValue().SetS(skValue));
    auto outcome = client.GetItem(request);
    if (!outcome.IsSuccess()) {
        std::cerr << "GetItem 실패: " << outcome.GetError().GetMessage() << std::endl;
        return std::nullopt;
    }
    auto item = outcome.GetResult().GetItem();
    if (item.empty()) return std::nullopt;
    return item;
}

GetItem의 기본 읽기는 최종적 일관성(eventually consistent)이라, 방금 쓴 아이템을 바로 읽으면 이전 값이 나올 수 있습니다. 쓰기 직후 읽기가 중요하다면 request.SetConsistentRead(true)를 주면 되고, 읽기 용량은 두 배로 소모됩니다.

UpdateItem: 속성 업데이트

#include <aws/dynamodb/model/UpdateItemRequest.h>
bool updateDynamoItem(const std::string& tableName,
                     const std::string& pkName, const std::string& pkValue,
                     const std::string& attrName, const std::string& attrValue,
                     const Aws::Client::ClientConfiguration& config) {
    Aws::DynamoDB::DynamoDBClient client(config);
    Aws::DynamoDB::Model::UpdateItemRequest request;
    request.SetTableName(tableName);
    request.AddKey(pkName, Aws::DynamoDB::Model::AttributeValue().SetS(pkValue));
    request.SetUpdateExpression("SET #a = :v");
    Aws::Map<Aws::String, Aws::String> exprNames;
    exprNames["#a"] = attrName;
    request.SetExpressionAttributeNames(exprNames);
    Aws::Map<Aws::String, Aws::DynamoDB::Model::AttributeValue> exprValues;
    exprValues[":v"] = Aws::DynamoDB::Model::AttributeValue().SetS(attrValue);
    request.SetExpressionAttributeValues(exprValues);
    auto outcome = client.UpdateItem(request);
    return outcome.IsSuccess();
}

#a, :v 같은 자리 표시자를 쓰는 이유는 두 가지입니다. :v는 값을 식 문자열과 분리해 타입을 명시하기 위한 것이고, #a는 속성 이름이 status, name, data 같은 DynamoDB 예약어와 겹칠 때 Attribute name is a reserved keyword 오류를 피하기 위한 것입니다. 예약어 목록이 수백 개라서, 속성 이름은 항상 # 별칭으로 넘기는 습관이 편합니다.

또 UpdateItem은 키에 해당하는 아이템이 없으면 새로 만듭니다(upsert). 존재하는 아이템만 수정하려던 의도라면 SetConditionExpression("attribute_exists(#pk)")를 추가해야, 잘못된 키로 호출했을 때 빈 아이템이 조용히 생기는 일을 막을 수 있습니다.

Query: 파티션 키 기준 조회

#include <aws/dynamodb/model/QueryRequest.h>
std::vector<Aws::Map<Aws::String, Aws::DynamoDB::Model::AttributeValue>>
queryDynamoByPartitionKey(const std::string& tableName,
                         const std::string& pkName, const std::string& pkValue,
                         const Aws::Client::ClientConfiguration& config) {
    Aws::DynamoDB::DynamoDBClient client(config);
    std::vector<Aws::Map<Aws::String, Aws::DynamoDB::Model::AttributeValue>> items;
    Aws::DynamoDB::Model::QueryRequest request;
    request.SetTableName(tableName);
    request.SetKeyConditionExpression("#pk = :pkval");
    request.AddExpressionAttributeNames("#pk", pkName);
    request.AddExpressionAttributeValues(":pkval",
        Aws::DynamoDB::Model::AttributeValue().SetS(pkValue));
    Aws::Map<Aws::String, Aws::DynamoDB::Model::AttributeValue> lastEvaluatedKey;
    do {
        if (!lastEvaluatedKey.empty()) {
            request.SetExclusiveStartKey(lastEvaluatedKey);
        }
        auto outcome = client.Query(request);
        if (!outcome.IsSuccess()) break;
        for (const auto& item : outcome.GetResult().GetItems()) {
            items.push_back(item);
        }
        lastEvaluatedKey = outcome.GetResult().GetLastEvaluatedKey();
    } while (!lastEvaluatedKey.empty());
    return items;
}

Lambda 함수 호출

Invoke (동기 호출)

#include <aws/lambda/LambdaClient.h>
#include <aws/lambda/model/InvokeRequest.h>
#include <aws/core/utils/json/JsonSerializer.h>
struct LambdaResponse {
    bool success;
    std::string payload;
    std::string errorMessage;
};
LambdaResponse invokeLambdaSync(const std::string& functionName,
                                const std::string& payloadJson,
                                const Aws::Client::ClientConfiguration& config) {
    Aws::Lambda::LambdaClient client(config);
    Aws::Lambda::Model::InvokeRequest request;
    request.SetFunctionName(functionName);
    request.SetInvocationType(Aws::Lambda::Model::InvocationType::RequestResponse);
    auto payloadStream = Aws::MakeShared<Aws::StringStream>("");
    *payloadStream << payloadJson;
    request.SetBody(payloadStream);
    auto outcome = client.Invoke(request);
    LambdaResponse result{};
    if (!outcome.IsSuccess()) {
        result.success = false;
        result.errorMessage = outcome.GetError().GetMessage();
        return result;
    }
    auto& invokeResult = outcome.GetResult();
    auto statusCode = invokeResult.GetStatusCode();
    // 함수 코드가 예외를 던져도 StatusCode는 200이며, FunctionError에 "Unhandled" 등이 담김
    if (!invokeResult.GetFunctionError().empty()) {
        result.success = false;
        result.errorMessage = "Function error: " + invokeResult.GetFunctionError();
        return result;
    }
    if (statusCode >= 200 && statusCode < 300) {
        result.success = true;
        auto& body = invokeResult.GetPayload();
        std::string payloadStr((std::istreambuf_iterator<char>(body)),
                              std::istreambuf_iterator<char>());
        result.payload = payloadStr;
    } else {
        result.success = false;
        result.errorMessage = "Lambda returned status " + std::to_string(statusCode);
    }
    return result;
}

InvocationType:

  • RequestResponse: 동기, 응답 대기 (최대 15분, 클라이언트 타임아웃 별도)
  • Event: 비동기, 즉시 반환
  • DryRun: 검증만 수행

동기 호출에서 가장 자주 놓치는 부분이 함수 오류의 표현 방식입니다. Lambda 함수 코드 안에서 예외가 발생해도 Invoke API 자체는 성공으로 처리되고 HTTP 상태도 200입니다. 대신 FunctionError 필드에 Unhandled가 담기고, 응답 본문에는 {"errorMessage": "...", "errorType": "..."} 형태의 오류 JSON이 들어옵니다. 원래 예제는 상태 코드만 보고 성공으로 판단했기 때문에, 함수가 실패해도 오류 JSON을 정상 결과로 받아 처리하는 버그가 있었습니다. 위처럼 GetFunctionError()를 반드시 확인해야 합니다.

Event 호출은 요청이 Lambda의 내부 큐에 들어가는 즉시 202를 반환하므로, 이 코드의 success는 “큐에 들어갔다”는 뜻일 뿐 함수가 성공했다는 뜻이 아닙니다. 비동기 호출이 실패하면 Lambda가 기본적으로 두 번 재시도하고, 그래도 실패하면 버려지므로 실패 알림이 필요하면 함수에 실패 대상(Destination)이나 DLQ를 설정해야 합니다. 또 재시도 때문에 같은 이벤트가 여러 번 처리될 수 있어 함수는 멱등하게 작성하는 것이 좋습니다. 페이로드 크기 제한은 동기 호출이 비동기 호출보다 넉넉하므로, 큰 데이터는 S3에 올리고 키만 넘기는 방식이 일반적입니다.

Invoke (비동기 Event)

LambdaResponse invokeLambdaAsync(const std::string& functionName,
                                 const std::string& payloadJson,
                                 const Aws::Client::ClientConfiguration& config) {
    Aws::Lambda::LambdaClient client(config);
    Aws::Lambda::Model::InvokeRequest request;
    request.SetFunctionName(functionName);
    request.SetInvocationType(Aws::Lambda::Model::InvocationType::Event);
    auto payloadStream = Aws::MakeShared<Aws::StringStream>("");
    *payloadStream << payloadJson;
    request.SetBody(payloadStream);
    auto outcome = client.Invoke(request);
    LambdaResponse result{};
    result.success = outcome.IsSuccess();
    if (!outcome.IsSuccess()) {
        result.errorMessage = outcome.GetError().GetMessage();
    }
    return result;
}

IAM 인증 및 Credentials

Credentials 체인 (기본 순서)

AWS SDK의 기본 자격 증명 공급자 체인은 대략 다음 순서로 자격 증명을 찾고, 처음 찾은 것을 씁니다(SDK 버전에 따라 세부 단계가 추가될 수 있습니다):

  1. 환경 변수: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN(선택)
  2. 공유 설정 파일: ~/.aws/credentials, ~/.aws/config의 프로파일 (AWS_PROFILE로 선택)
  3. 웹 ID 토큰(EKS의 IRSA 등), SSO, 외부 프로세스 공급자
  4. ECS/컨테이너 자격 증명 (Task Role)
  5. EC2 Instance Profile (EC2 메타데이터)

순서가 중요한 이유는 앞에서 찾으면 뒤는 보지 않기 때문입니다. EC2에 Instance Profile을 붙였는데도 권한 오류가 난다면, 배포 스크립트나 컨테이너 이미지에 남은 AWS_ACCESS_KEY_ID 환경 변수가 우선 적용되고 있는지 먼저 확인합니다. 로컬에서는 되는데 서버에서만 실패하는 문제의 상당수가 이런 “예상과 다른 자격 증명”입니다. 어떤 자격 증명이 쓰이는지는 같은 환경에서 aws sts get-caller-identity를 실행하면 바로 확인할 수 있습니다.

# 환경 변수 설정 예시
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_DEFAULT_REGION=ap-northeast-2

프로파일 지정

Aws::Client::ClientConfiguration config;
config.region = "ap-northeast-2";
// 특정 프로파일 사용 (예: ~/.aws/credentials의 [my-profile])
// 환경 변수 AWS_PROFILE=my-profile 로도 가능

EC2/ECS에서 IAM Role

EC2에 Instance Profile을, ECS Task에 Task Role을 붙이면 별도 키 없이 SDK가 자동으로 메타데이터 서비스에서 임시 자격 증명을 가져옵니다. 프로덕션에서는 이 방식을 권장합니다.


NoSuchBucket, SignatureDoesNotMatch, ValidationException: 에러 해결

S3: “NoSuchBucket” / “Access Denied”

원인: 버킷 없음, 버킷명 오타, IAM 권한 부족 해결법:

// 버킷명·리전 확인
config.region = "ap-northeast-2";  // 버킷이 생성된 리전과 일치
// IAM 정책에 s3:PutObject, s3:GetObject, s3:ListBucket 필요

필요 IAM 권한 예시:

{
  "Effect": "Allow",
  "Action": [
    "s3:PutObject",
    "s3:GetObject",
    "s3:DeleteObject",
    "s3:ListBucket"
  ],
  "Resource": [
    "arn:aws:s3:::my-bucket",
    "arn:aws:s3:::my-bucket/*"
  ]
}

S3: “SignatureDoesNotMatch”

원인: 시크릿 키 오타, 시스템 시계 불일치, 리전·서비스명 불일치 해결법:

  • 시크릿 키 재확인
  • chrony나 systemd-timesyncd(timedatectl status로 확인)로 서버 시간 동기화. 서명 시각이 서버 시각과 약 15분 이상 차이 나면 RequestTimeTooSkewed로 거부됩니다.
  • endpointOverride 사용 시 서비스명·리전 일치 확인

DynamoDB: “ValidationException: One or more parameter values were invalid”

원인: AttributeValue 타입이 테이블 스키마와 다름 해결법:

// 잘못된 예: 파티션 키가 문자열인데 숫자로 전달
request.AddKey("userId", Aws::DynamoDB::Model::AttributeValue().SetN("123"));  // 스키마가 S면 SetS
// 올바른 예: 스키마에 맞게
request.AddKey("userId", Aws::DynamoDB::Model::AttributeValue().SetS("user-123"));
request.AddKey("timestamp", Aws::DynamoDB::Model::AttributeValue().SetN("1700000000"));

DynamoDB: “ResourceNotFoundException”

원인: 테이블명 오타, 리전 불일치, 테이블이 아직 ACTIVE가 아님 해결법:

  • 테이블명·리전 확인
  • 테이블 생성 직후 DescribeTable로 TableStatus == ACTIVE 확인 후 사용

Lambda: “ResourceNotFoundException”

원인: 함수명 오타, 리전 불일치, 함수가 다른 리전에 있음 해결법:

config.region = "ap-northeast-2";  // Lambda 함수가 있는 리전
// 함수명: my-function 또는 my-function:alias

Lambda: 타임아웃

원인: Lambda 자체 타임아웃(최대 15분), 클라이언트 requestTimeoutMs 부족 해결법:

config.requestTimeoutMs = 60000;  // 60초 (Lambda 실행 시간 + 네트워크)
// Lambda 콘솔에서 함수 타임아웃도 충분히 설정

공통: “RequestTimeout” / “Connection timeout”

원인: 네트워크 지연, 방화벽, DNS 문제, requestTimeoutMs 짧음 해결법:

config.connectTimeoutMs = 5000;
config.requestTimeoutMs = 30000;
// 재시도: RetryStrategy 설정 (기본적으로 SDK가 재시도함)

클라이언트 재사용·병렬 멀티파트·Batch API

클라이언트 재사용

클라이언트는 스레드 세이프합니다. 앱 전체에서 재사용하세요. 단, 아래처럼 함수 내부 static으로 두면 앞에서 설명한 대로 프로그램 종료 시 ShutdownAPI 이후에 소멸하므로, 종료 시 크래시가 나면 이 패턴부터 의심해야 합니다.

class AwsServiceHolder {
public:
    static Aws::S3::S3Client& getS3() {
        static Aws::S3::S3Client client(getConfig());
        return client;
    }
    static Aws::DynamoDB::DynamoDBClient& getDynamo() {
        static Aws::DynamoDB::DynamoDBClient client(getConfig());
        return client;
    }
private:
    static Aws::Client::ClientConfiguration getConfig() {
        Aws::Client::ClientConfiguration c;
        c.region = "ap-northeast-2";
        c.maxConnections = 100;
        return c;
    }
};

S3 멀티파트 업로드 병렬화

대용량 파일은 파트별로 std::async 등으로 병렬 업로드할 수 있습니다.

// 파트 업로드를 std::async로 병렬 실행
std::vector<std::future<PartETag>> futures;
for (size_t i = 0; i < numParts; ++i) {
    futures.push_back(std::async(std::launch::async, [&, i]() {
        return uploadPart(uploadId, i + 1, ...);
    }));
}
for (auto& f : futures) {
    parts.push_back(f.get());
}

DynamoDB BatchGetItem / BatchWriteItem

여러 아이템을 한 번에 조회·쓰기하면 RTT를 줄일 수 있습니다.

#include <aws/dynamodb/model/BatchGetItemRequest.h>
// BatchGetItem: 최대 100개 키, 16MB 응답 제한
Aws::DynamoDB::Model::BatchGetItemRequest batchReq;
// KeysAndAttributes에 테이블별 키 목록 추가

Connection Pool 설정

config.maxConnections = 100;  // 동시 연결 수
config.connectTimeoutMs = 3000;
config.requestTimeoutMs = 30000;

S3 TransferManager (고수준 API)

AWS SDK는 aws-cpp-sdk-transfer 컴포넌트의 TransferManager로 멀티파트·병렬 업로드/다운로드를 자동화합니다. 파일 크기에 따라 단일 PutObject와 멀티파트를 알아서 고르고, 스레드 풀(Aws::Utils::Threading::PooledThreadExecutor)로 파트를 병렬 전송하며, 진행 상황 콜백과 실패한 파트의 재시도, 취소 시 AbortMultipartUpload까지 처리합니다. 앞의 직접 구현은 동작 원리를 이해하는 용도로 보고, 대용량 전송은 이쪽을 쓰는 것이 유지보수에 유리합니다.

위의 std::async 병렬화 스케치는 파트 수만큼 스레드를 만들므로, 1,000파트짜리 파일이면 스레드 1,000개가 동시에 생깁니다. 직접 병렬화한다면 동시 실행 수를 maxConnections 이하로 제한하는 작업 큐가 필요합니다.


재시도 정책·환경별 설정·메트릭

에러 처리 및 로깅

bool safePutObject(Aws::S3::S3Client& client,
                  const std::string& bucket, const std::string& key,
                  std::shared_ptr<Aws::IOStream> body) {
    try {
        Aws::S3::Model::PutObjectRequest request;
        request.SetBucket(bucket);
        request.SetKey(key);
        request.SetBody(body);
        auto outcome = client.PutObject(request);
        if (!outcome.IsSuccess()) {
            // 구조화된 로깅 (CloudWatch Logs 등)
            std::cerr << "[S3_ERROR] " << outcome.GetError().GetExceptionName()
                      << ": " << outcome.GetError().GetMessage()
                      << " (bucket=" << bucket << ", key=" << key << ")"
                      << std::endl;
            return false;
        }
        return true;
    } catch (const std::exception& e) {
        std::cerr << "[S3_EXCEPTION] " << e.what() << std::endl;
        return false;
    }
}

재시도 정책

SDK 기본 재시도는 지수 백오프를 사용합니다. RetryStrategy를 커스터마이즈할 수 있습니다.

#include <aws/core/client/DefaultRetryStrategy.h>
Aws::Client::ClientConfiguration config;
config.retryStrategy = Aws::MakeShared<Aws::Client::DefaultRetryStrategy>(
    "custom", 3, 1000);  // 최대 3회 재시도, 초기 딜레이 1초

환경별 설정

std::string getAwsRegion() {
    const char* env = std::getenv("AWS_REGION");
    if (env && strlen(env) > 0) return env;
    return "ap-northeast-2";  // 기본값
}
// 로컬스택 등 커스텀 엔드포인트
void setupLocalStack(Aws::Client::ClientConfiguration& config) {
    const char* endpoint = std::getenv("AWS_ENDPOINT_URL");
    if (endpoint) {
        config.endpointOverride = endpoint;
    }
}

헬스체크

bool checkS3Connectivity(const Aws::Client::ClientConfiguration& config) {
    Aws::S3::S3Client client(config);
    Aws::S3::Model::ListBucketsRequest request;
    auto outcome = client.ListBuckets(request);
    return outcome.IsSuccess();
}

메트릭·모니터링

  • CloudWatch Metrics: 애플리케이션에서 요청 수·지연·에러 수를 직접 집계해 보내는 것이 일반적입니다(S3, DynamoDB, Lambda 쪽 서버 지표는 CloudWatch에 기본으로 쌓입니다)
  • X-Ray: 분산 추적 활성화 시 세그먼트 기록
  • 로그: 구조화된 JSON 로그로 CloudWatch Logs에 전송

서비스별 연동 점검 항목

환경 설정

  • AWS SDK for C++ 설치 (vcpkg 또는 시스템 패키지)
  • CMake에 s3, dynamodb, lambda, core 컴포넌트 링크
  • Aws::InitAPI / ShutdownAPI 호출

Credentials

  • 로컬: ~/.aws/credentials 또는 환경 변수 설정
  • EC2/ECS: Instance Profile / Task Role 할당
  • IAM 정책: S3, DynamoDB, Lambda 필요한 권한 포함

S3

  • 버킷 생성 및 리전 확인
  • 5MB 이상 파일은 멀티파트 업로드 사용
  • 에러 시 AbortMultipartUpload 호출

DynamoDB

  • 테이블 스키마 확인 (파티션 키·정렬 키 타입)
  • AttributeValue 타입 일치 (SetS vs SetN)
  • Query 시 KeyConditionExpression 올바른 사용

Lambda

  • 함수명·리전 일치
  • InvocationType에 맞는 타임아웃 설정
  • IAM에 lambda:InvokeFunction 권한

프로덕션

  • 클라이언트 재사용 (싱글톤 등)
  • 에러 로깅·모니터링
  • 재시도 정책 검토
  • 환경별 설정 분리 (dev/staging/prod)

서비스별 요약

항목설명
S3PutObject, GetObject, 멀티파트 업로드, ListObjectsV2
DynamoDBPutItem, GetItem, UpdateItem, Query, BatchGetItem
LambdaInvoke (RequestResponse / Event)
IAM환경 변수, Instance Profile, credentials 체인
에러NoSuchBucket, ValidationException, ResourceNotFoundException, 타임아웃
성능클라이언트 재사용, 멀티파트 병렬화, BatchGetItem
프로덕션로깅, 재시도, 환경별 설정, 헬스체크

핵심 원칙:

  1. SDK 초기화·종료를 반드시 수행
  2. AttributeValue·스키마 타입 일치
  3. 리전·엔드포인트·함수명 확인
  4. 프로덕션에서는 IAM Role 사용
  5. 클라이언트 재사용으로 성능 확보

자주 묻는 질문 (FAQ)

Q. S3 호출에서 SignatureDoesNotMatch가 나는데 키는 맞습니다. 무엇을 확인해야 하나요?

A. 요청 서명에는 시크릿 키뿐 아니라 요청 시각, 리전, 서비스명이 함께 들어가므로 키가 맞아도 서명이 어긋날 수 있습니다. 서버 시계가 틀어져 있으면 서명이 거부되므로 NTP로 시간을 동기화하고, endpointOverride를 쓰는 경우 ClientConfiguration의 리전과 실제 버킷 리전이 일치하는지 확인해야 합니다. 그래도 안 되면 시크릿 키에 공백이나 줄바꿈이 섞여 들어가지 않았는지 다시 확인합니다.

Q. DynamoDB 호출에서 ValidationException: One or more parameter values were invalid가 나면?

A. 대부분 AttributeValue의 타입이 테이블 스키마와 다를 때 납니다. 파티션 키가 문자열(S)로 정의되어 있는데 SetN으로 숫자를 넘기는 식입니다. 테이블의 키 스키마를 확인하고 문자열은 SetS, 숫자는 SetN으로 스키마와 같은 타입을 넘기면 해결됩니다.

Q. 프로그램이 끝날 때만 세그폴트가 납니다.

A. 거의 항상 SDK 객체의 수명 문제입니다. 서비스 클라이언트나 SDK 타입(Aws::String 등을 담은 객체)이 Aws::ShutdownAPI 이후에 소멸하는지 확인하세요. main의 지역 변수는 중괄호 블록 안에 두고, 전역·정적 클라이언트는 ShutdownAPI 전에 명시적으로 해제해야 합니다.

C++에서 S3·DynamoDB·Lambda를 연동할 때는 SDK 초기화·종료 순서, 스트림 기반 요청 본문, FunctionError와 ConditionExpression 같은 서비스별 의미를 정확히 알아야 조용한 오류를 피할 수 있습니다.


같이 보면 좋은 글