AWS Lambda + API Gateway Cold Start 줄이는 실전 전략
Cold Start는 Lambda 함수가 오랫동안 호출되지 않다가 새 요청이 들어올 때, AWS가 컨테이너를 새로 초기화하면서 발생하는 지연 현상이다. API Gateway와 조합하면 이 지연이 그대로 HTTP 응답 시간에 반영되기 때문에, 특히 실시간 응답이 중요한 서비스에서는 눈에 띄게 체감된다.
문제는 단순히 ‘느리다’에서 끝나지 않는다. 모바일 앱의 첫 화면 로딩, 결제 API 호출, 로그인 요청처럼 사용자가 직접 기다리는 구간에 Cold Start가 끼어들면 이탈률 상승으로 이어질 수 있다. 그래서 이 문제를 어떻게 줄일 수 있는지 구체적인 방법을 짚어보는 게 의미 있다.
Cold Start가 실제로 얼마나 걸리나
Cold Start 시간은 런타임 종류, 함수 크기, VPC 설정 여부에 따라 크게 달라진다. 일반적으로 Python이나 Node.js 기반 함수는 수백 밀리초 수준이지만, Java나 .NET 같은 JVM 기반 런타임은 초 단위를 넘기는 경우도 있다. 여기에 VPC 내부에서 실행되는 함수라면 ENI(Elastic Network Interface) 연결 과정이 추가되어 지연이 더 길어진다.
AWS 공식 문서에서도 VPC Lambda의 Cold Start 지연이 과거보다 많이 개선되었다고 밝히고 있으며, 2019년 이후 ENI 재사용 방식이 개선되면서 수 초에 달하던 지연이 줄었다. 그러나 여전히 VPC 없이 실행하는 것보다는 오래 걸린다.
Cold Start를 측정하려면 AWS CloudWatch의 INIT_START 로그 이벤트를 확인하는 것이 가장 정확하다. Lambda 함수에서 --log-type Tail 옵션을 사용하거나, CloudWatch Logs Insights에서 @initDuration 필드를 쿼리하면 초기화 시간을 별도로 확인할 수 있다.
런타임 선택이 Cold Start에 미치는 영향
언어 선택은 Cold Start에 직접적인 영향을 준다. 인터프리터 방식으로 동작하는 Python과 Node.js는 초기화 속도가 빠른 편이다. 반면 Java, Kotlin, Scala처럼 JVM 위에서 동작하는 언어는 클래스 로딩과 JIT 컴파일 과정이 추가되기 때문에 초기화 시간이 길다.
Java를 사용해야 하는 상황이라면 GraalVM Native Image로 컴파일하는 방법이 있다. 네이티브 이미지로 빌드하면 JVM 없이 직접 실행되는 바이너리가 생성되어 Cold Start를 대폭 줄일 수 있다. AWS에서는 이를 위한 Custom Runtime(provided.al2) 환경을 지원하고 있으며, Quarkus나 Micronaut 프레임워크도 GraalVM 네이티브 빌드를 공식 지원한다.
Node.js나 Python을 쓰더라도 불필요한 패키지를 함수 패키지에 포함시키면 초기화 시간이 늘어난다. Lambda 함수의 패키지 크기를 줄이는 것 자체가 Cold Start를 줄이는 방법 중 하나다.
패키지 최적화: 크기를 줄이는 실질적인 방법
Lambda 함수는 실행 전에 패키지를 S3에서 다운로드하고 압축을 해제한다. 패키지가 클수록 이 과정이 길어지고 Cold Start에도 영향을 준다.
의존성 정리
- Node.js:
npm ci --production으로 devDependency를 제외하고, 번들러(esbuild, webpack)로 tree-shaking 적용 - Python:
pip install --target시 불필요한 패키지 제외, 또는 Lambda Layer로 분리 - Java: Uber JAR 대신 Lambda Web Adapter나 레이어 분리 방식 고려
Lambda Layer 활용
공통 의존성은 Lambda Layer로 분리하면 함수 패키지 자체의 크기를 줄일 수 있다. 레이어는 최대 5개까지 연결 가능하며, 여러 함수에서 공유된다. 단, 레이어도 초기화 시 로드되므로 레이어 수를 무작정 늘리면 오히려 역효과가 날 수 있다.
Container Image 사용 시 주의
Docker 컨테이너 이미지로 Lambda를 배포하면 최대 10GB까지 사용 가능하지만, 이미지가 클수록 Cold Start가 길어진다. 베이스 이미지는 AWS 공식 Lambda 베이스 이미지를 사용하는 것이 좋고, 멀티 스테이지 빌드로 최종 이미지 크기를 최소화하는 것이 권장된다.
Provisioned Concurrency로 Cold Start 완전 제거
Cold Start를 근본적으로 없애고 싶다면 Provisioned Concurrency가 가장 확실한 방법이다. 특정 수의 함수 인스턴스를 미리 초기화된 상태로 유지해두기 때문에 요청이 들어오는 즉시 처리할 수 있다.
Provisioned Concurrency는 Lambda 함수의 특정 버전 또는 별칭(Alias)에 설정할 수 있으며, Auto Scaling과 연동하면 트래픽 패턴에 따라 자동으로 조정할 수 있다.
aws lambda put-provisioned-concurrency-config \
--function-name my-function \
--qualifier my-alias \
--provisioned-concurrent-executions 10
단점은 비용이다. Provisioned Concurrency는 요청이 없어도 인스턴스가 대기 중인 시간만큼 요금이 청구된다. 따라서 24시간 항상 요청이 있는 API라면 비용 대비 효과가 좋지만, 간헐적으로만 호출되는 함수에 설정하면 비효율적이다.
Application Auto Scaling을 통해 스케줄 기반으로 피크 시간대에만 Provisioned Concurrency를 높이고 새벽 시간대에는 줄이는 방식이 비용 최적화에 도움이 된다.
코드 레벨 최적화: 핸들러 외부로 꺼내기
Cold Start 시간의 일부는 함수 초기화 코드, 즉 핸들러 함수 밖에서 실행되는 코드가 차지한다. 반대로 이 특성을 활용하면 초기화 비용을 최소화할 수 있다.
데이터베이스 연결, SDK 클라이언트 초기화, 설정 파일 로드 같은 작업을 핸들러 내부에서 매 요청마다 실행하면 당연히 느려진다. 이런 작업은 핸들러 함수 바깥, 즉 모듈 레벨에서 한 번만 실행되도록 옮겨야 한다.
import boto3
# 핸들러 바깥에서 초기화 - 컨테이너 재사용 시 재실행되지 않음
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('my-table')
def handler(event, context):
# 이미 초기화된 table 객체 바로 사용
response = table.get_item(Key={'id': event['id']})
return response
Lambda 컨테이너가 재사용될 때는 핸들러 외부 코드가 다시 실행되지 않으므로, 이미 연결된 클라이언트를 그대로 활용할 수 있다. 단, 이 방식을 쓸 때는 연결 객체가 만료되거나 오류가 발생했을 때를 처리하는 로직도 함께 고려해야 한다.
SnapStart로 Java Cold Start 대응하기
AWS Lambda SnapStart는 Java 함수의 Cold Start 문제를 해결하기 위해 도입된 기능이다. 함수를 배포할 때 초기화 과정을 미리 실행하고, 그 메모리 상태를 스냅샷으로 저장해둔다. 이후 요청이 들어오면 스냅샷 상태에서 바로 복원하여 실행하기 때문에 초기화 과정을 건너뛸 수 있다.
SnapStart는 현재 Java 11 이상의 Corretto 런타임에서 지원되며, 함수 버전을 게시(Publish)해야 활성화할 수 있다. 단, 스냅샷 복원 시 네트워크 연결이나 난수 생성 관련 상태가 달라질 수 있어, CacheScope.GLOBAL을 활용하거나 BeforeCheckpoint, AfterRestore 훅을 적절히 구현해야 한다.
API Gateway 설정도 지연에 영향을 준다
Cold Start는 Lambda 측 문제만이 아니다. API Gateway 자체의 설정도 전체 응답 시간에 영향을 준다.
HTTP API vs REST API
API Gateway에는 크게 REST API와 HTTP API 두 가지가 있다. HTTP API는 REST API보다 레이턴시가 낮고 비용도 저렴하다. Lambda 통합 방식도 HTTP API의 경우 payload format v2.0을 사용하면 더 간결하게 처리된다. 복잡한 기능(WAF 통합, 요청 검증 등)이 필요하지 않다면 HTTP API를 선택하는 것이 기본 레이턴시를 줄이는 데 유리하다.
리전 엔드포인트 vs 엣지 최적화 엔드포인트
API Gateway 엔드포인트 유형도 레이턴시에 영향을 준다. 엣지 최적화(Edge-Optimized) 엔드포인트는 CloudFront를 통해 라우팅되므로 지리적으로 멀리 떨어진 사용자에게 유리할 수 있지만, 내부 서비스나 특정 리전 사용자만 대상이라면 리전(Regional) 엔드포인트가 더 직접적이고 빠를 수 있다.
Warm-Up 요청 방식과 그 한계
Provisioned Concurrency를 사용하지 않는 환경에서 자주 쓰이는 방법이 Warm-Up 요청이다. CloudWatch Events(EventBridge)를 통해 일정 주기로 Lambda 함수를 호출하여 컨테이너가 종료되지 않도록 유지하는 방식이다.
이 방법은 비용이 거의 들지 않고 구현이 단순하다는 장점이 있다. 하지만 몇 가지 제약이 있다.
- 트래픽이 갑자기 늘어날 때 새로 생성되는 인스턴스는 여전히 Cold Start를 겪는다.
- Lambda가 내부적으로 컨테이너를 언제 회수할지는 AWS가 결정하므로, Warm-Up 주기를 짧게 해도 완전히 보장되지 않는다.
- 동시성이 높은 환경에서는 Warm-Up 요청 하나로는 충분하지 않다. 동시에 여러 인스턴스를 유지하려면 다수의 병렬 Warm-Up 호출이 필요하다.
따라서 Warm-Up은 보조 수단으로는 쓸 수 있지만, 안정적인 Cold Start 대응을 위해서는 Provisioned Concurrency와 함께 사용하거나 대체해야 한다.
메모리 설정이 성능에 미치는 효과
Lambda의 메모리 설정은 단순히 RAM 크기만 조절하는 것이 아니다. 메모리를 높이면 비례하여 CPU 할당도 늘어난다. 즉, 메모리를 올리면 함수 초기화 속도 자체가 빨라지는 효과가 있다.
실제로 128MB로 설정된 함수보다 512MB나 1024MB로 설정된 함수의 Cold Start가 눈에 띄게 짧은 경우가 많다. AWS Lambda Power Tuning 도구(Step Functions 기반 오픈소스)를 사용하면 다양한 메모리 설정에서의 실행 시간과 비용을 자동으로 측정하고 최적 설정을 찾아준다.
비용 측면에서 메모리를 올리면 요금이 올라가지만, 실행 시간이 줄어들기 때문에 GB-초 기준 전체 비용이 오히려 낮아지는 경우도 있다. 단순히 메모리를 최소로 유지하는 것이 비용 절감이라고 단정할 수 없다.
전략 선택 기준 정리
어떤 방법을 쓸지는 서비스 특성에 따라 달라진다. 아래 표는 상황별 권장 전략을 정리한 것이다.
| 상황 | 권장 전략 |
|---|---|
| Java 런타임, 짧은 Cold Start 필요 | SnapStart 또는 GraalVM Native Image |
| 피크 시간이 명확한 서비스 | 스케줄 기반 Provisioned Concurrency |
| 24시간 안정적인 응답 필요 | Provisioned Concurrency 고정 설정 |
| 비용 최소화, 간헐적 트래픽 | 메모리 최적화 + Warm-Up 보조 사용 |
| VPC 내부 실행 | VPC 필요 여부 재검토, 또는 VPC 엔드포인트 활용 |
| 패키지 크기가 큰 경우 | 의존성 정리, Lambda Layer 분리 |
Cold Start는 단일 원인으로 발생하지 않기 때문에 하나의 방법만으로 해결되지 않는 경우가 많다. 런타임 선택, 패키지 최적화, 코드 구조 개선, Provisioned Concurrency 조합을 상황에 맞게 적용하는 것이 실질적으로 효과를 낸다.