🔥 Lambda란 무엇인가: 서버리스의 경계

1704자
21분

16:9 가로 표지 일러스트레이션. 흰 배경 중앙에 'AWS Lambda, 서버리스의 경계'라는 제목이 있다. 아래에는 Python, Node.js, Java, Ruby, .NET, Container 런타임 배지가 가로로 놓여 있다.

처음 람다 함수를 콘솔에서 호출하면 응답까지 수백 ms에서 1초를 넘기는 콜드스타트가 한 번 들어간다. 같은 함수를 두 번째로 부르면 그 비용이 없어지고 핸들러 본체가 도는 시간만 남는다. 같은 코드인데, 첫 호출과 두 번째 호출 사이에 AWS가 어떤 실행 환경을 새로 준비했고, 두 번째 호출은 그 실행 환경이 살아 있는 채로 도착했다는 뜻이다.

EC2 인스턴스를 띄우면 물리 서버, Nitro 하이퍼바이저, 인스턴스, 게스트 OS, 앱까지 다섯 단계가 함께 올라온다. Lambda에서는 AWS가 앞의 네 단계를 맡고, 사용자는 함수 코드와 설정만 다룬다. 그래서 비용과 운영 부담의 기준도 인스턴스 유지가 아니라 함수 실행으로 바뀐다.

다섯 단계 중 어디까지를 AWS가 가져갔나

EC2를 한 줄 띄우면 다섯 단계, 즉 물리 서버, Nitro 하이퍼바이저, 인스턴스, 게스트 OS, 앱이 위에서 아래로 줄지어 선다. 그중 위 세 단계(인스턴스·OS·앱)가 사용자 책임 영역에 들어가고, 한 사람이 매주 OS 패치와 AMI 관리에 시간을 쓰게 된다. 지난 편에서 본 책임 스펙트럼의 왼쪽 끝이 그 지점이었다.

Lambda는 그 다섯 단계 중 가장 위 한 단계(앱) 안에서도 핸들러 함수 한 묶음만 사용자에게 남기고 나머지를 모두 AWS 안으로 가져간 구조다. 인스턴스 단위로 띄우는 행위 자체를 사용자가 더 이상 하지 않는다. OS 패치도, 런타임 업그레이드도, 인스턴스 대수를 키우거나 줄이는 ASG 설정도 사용자 영역 바깥으로 밀려난다. 콘솔에서 Create function을 누르면 Python 3.13 한 줄을 고르고, 함수 본문 한 묶음을 붙여 넣고, Save를 누른다. 그게 끝이다.

EC2와 Lambda 책임 비교 그림. 왼쪽에는 물리 서버부터 앱까지 다섯 단계가 있고, 오른쪽에는 AWS가 네 단계를 맡고 사용자는 함수 코드만 맡는다.

왜 AWS는 그 영역까지 떠안는 구조를 만들었을까. 짧고 띄엄띄엄 들어오는 호출에 EC2 인스턴스 한 대를 24시간 켜 두면 비효율이 크다. 한 사용자 행동이 1분에 한 번 들어오는 이미지 썸네일 변환 워크로드를 가정해 보자. 변환 한 번에 0.3초가 걸리면, 인스턴스는 60초 중 0.3초만 일하고 59.7초는 idle 상태로 남는다. CPU 시간의 99.5%가 청구서로 새는 셈인데, 그 99.5%를 회수하려면 한 인스턴스 위에서 여러 사용자의 함수를 시간 단위로 잘게 섞어 띄워야 한다. 이걸 사용자가 직접 짜는 것보다 AWS가 fleet 단위로 가져가는 게 단가가 낮다. Lambda가 등장한 이유가 거기 있다.

Lambda가 받는 코드의 조건

코드만 가져왔다는 말은 부드럽지만, 그 아래에는 코드가 어떤 구조여야 하는지를 정해 두는 두 줄이 같이 따라온다.

첫째, 한 호출이 최대 900초(15분) 안에 끝나야 한다. 둘째, 핸들러는 이벤트 한 번에 진입했다 빠져나오는 stateless 함수여야 한다. 즉 정확성을 위해 호출 사이에 메모리·디스크 상태에 의존하면 안 된다. 실행 환경이 이어지는 동안에는 핸들러 바깥 객체와 /tmp 캐시를 다시 쓸 수 있지만, AWS가 실행 환경을 회수하는 순간 그 데이터도 함께 날아간다([공식 문서는 이런 재사용 패턴을 권한다).

15분이 경계가 된 이유는 실행 환경의 수명 자체에서 온다. Lambda 컨테이너 한 개는 invoke가 끝나면 즉시 없어지지 않고 잠깐 살아 있다가 다음 호출을 기다리는데, 무한 루프를 허용하면 idle 컨테이너가 fleet 안에서 계속 쌓여 다른 사용자의 콜드스타트 시간이 늘어난다. AWS는 그 한도를 15분으로 잘랐다. 8GB짜리 비디오 트랜스코딩 한 작업(평균 30~40분)을 Lambda에 넣으면 15분 timeout에 걸려 중간에 잘리고, 사용자 화면에는 변환 실패가 뜬다. 답은 AWS BatchECS task, 또는 Step Functions로 분할이지 Lambda가 아니다.

Stateless가 경계가 된 이유도 비슷하다. AWS는 같은 함수를 동시에 여러 컨테이너에 분산해 돌리는데, 컨테이너 A의 메모리 변수는 컨테이너 B에 닿지 않는다. /tmp 영역은 한 실행 환경에 한정된 임시 스토리지라 AWS가 컨테이너를 reap하면 같이 없앤다. 한 사용자가 첫 호출에서 count = 0을 만들고 매번 count += 1을 한 다음 return count를 했다고 치자. 어떤 호출은 1을 받고 어떤 호출은 7을 받고 어떤 호출은 1을 다시 받는다. 같은 컨테이너가 반복되면 카운터가 증가하지만 AWS가 새 컨테이너를 띄우면 0부터 다시 시작한다. 상태는 컨테이너 안이 아니라 DynamoDB 같은 바깥 저장소에 둬야 한다.

30분 넘게 도는 ETL, 상태를 오래 붙잡아야 하는 WebSocket 서버, GPU 추론은 이 조건 밖에 있다. 그래서 답은 Lambda가 아니라 다른 도구다. 디테일은 언제 EC2가 아닌 다른 걸 써야 하는가에서 본 결정 트리에 정리해 두었고, Lambda가 답이 아닌 경우는 언제 Lambda가 아닌 ECS/Fargate를 써야 하는가에서 다시 다룬다.

경계 안에서 코드가 도는 방식: 실행 환경 수명

경계 안에 들어온 코드는 공식 문서 기준 세 단계, 즉 Init, Invoke, Shutdown을 따라 돈다. SnapStart를 켠 함수만 Init 직후에 Restore 한 단계가 추가로 들어간다.

Lambda 실행 환경 수명 그림. Init 뒤에 Invoke가 반복되고 마지막에 Shutdown이 온다. SnapStart를 켠 함수에는 Restore가 먼저 온다.

Init 단계에서는 컨테이너를 fleet에서 한 개 골라 잡고, 그 위에 런타임(예: Python 3.13)을 부팅하고, 배포된 함수 코드(zip 패키지나 컨테이너 이미지)를 받아 풀고, init 코드(import 문, 글로벌 변수, DB 클라이언트 생성)를 한 번 실행한다. 이 단계가 첫 호출에 한 번 들어가는 콜드스타트의 정체다. 메커니즘 자체는 Cold Start: 왜 첫 호출이 느린가에서 깊이 다룬다.

Restore 단계SnapStart를 켠 함수에서만 등장하는 추가 단계다. AWS는 init 끝에 실행 환경의 메모리·디스크 스냅샷을 떠 두고, 다음 콜드스타트는 새 컨테이너를 처음부터 부팅하는 대신 스냅샷을 복원해 거기서 출발한다. Java 11+ / Python 3.12+ / .NET 8+ 세 갈래가 지원되며, AWS 표현으로 "as low as sub-second startup performance"가 가능하다. 단, init 시점에 만든 connection은 restore 후 살아 있다는 보장이 없고 unique content(랜덤 시드 등)가 여러 인스턴스에 그대로 복제되므로, snapshot-aware 핸들러를 따로 짜야 하는 경우가 있다.

Invoke 단계가 실제 핸들러 호출이다. AWS는 한 번 호출이 끝나도 컨테이너를 바로 버리지 않고 freeze 상태로 잠시 남겨 둔다. 다음 호출이 같은 컨테이너에 떨어지면 Init을 건너뛰고 바로 핸들러로 들어가는데, 이게 warm 상태다. 분당 10회 호출이 같은 함수에 떨어지는 API 엔드포인트는 첫 호출만 콜드, 나머지 아홉 번은 warm으로 핸들러 본체 시간만 들이고 끝난다.

Shutdown 단계는 컨테이너가 idle 상태로 한참 지속되거나 fleet이 재배치될 때 들어간다(AWS는 일반 idle retention 시간을 공식 수치로 공개하지 않는다). AWS가 컨테이너를 reap하기 전에 SHUTDOWN 이벤트를 보내고, 함수에 등록된 external extension은 최대 2,000ms 안에 cleanup(파일 flush, connection close)을 끝내야 한다.

이 흐름이 사용자에게 어떻게 나타나는지 한 시나리오로 살펴보자. 분당 10회 호출이 들어오는 API는 첫 invoke만 콜드, 나머지 아홉 번은 warm 응답이다. 그런데 마케팅 캠페인으로 갑자기 초당 1,000회가 들어오면 AWS는 fleet에 새 컨테이너 100개를 동시에 만들어 띄우고, 그 100개 모두가 각각 Init을 거친다. 첫 100개 응답이 콜드스타트 spike를 그리고, 사용자는 그 순간을 어쩌다 한 번 느린 응답 한 번으로 체감한다. 이게 burst 단위 콜드스타트다.

Lambda에 코드를 올리는 두 방식: 관리형 런타임과 컨테이너 이미지

코드를 가져오는 길은 두 갈래다. 관리형 런타임을 쓰거나, 컨테이너 이미지를 직접 만들어 ECR에 올리거나.

Lambda 코드 배포 방식 비교 그림. 왼쪽은 관리형 런타임 번들, 오른쪽은 컨테이너 이미지다.

관리형 런타임 갈래는 2026년 4월 29일 기준으로 공식 표에 여러 갈래가 올라가 있다. Python 3.14·3.13·3.12, Node.js 24·22·20 (Node.js 18은 2025-09-01에 deprecation되었고 함수 생성 차단은 2026-08-31, 업데이트 차단은 2026-09-30), Java 25·21 Corretto (Java 8·11·17은 Q2 2026 안에 AL2023 기반으로 다시 출시 예정), Ruby 3.4·3.3, .NET 10·8 (.NET 9는 container-only로만 지원), 그리고 OS-only / Custom Runtime은 Amazon Linux 2023이 깔린 provided.al2023 또는 provided.al2 위에 사용자가 부트스트랩 스크립트를 짜 넣는 길이다. 관리형 런타임은 OS 패치, 보안 업데이트, 런타임 부팅까지 모두 AWS가 가져간다. 대신 새 언어 버전이 나와도 LTS 단계에 도달해야 Lambda가 도입한다.

컨테이너 이미지 길은 자유와 책임이 같이 따라온다. 최대 10GB짜리 OCI 이미지를 ECR에 푸시하면 Lambda가 그 이미지를 invoke한다. 관리형 런타임에 없는 언어(Rust, Elixir, Crystal, Go도 OS-only/custom-runtime 길로 들어온다), 큰 ML 라이브러리(PyTorch, TensorFlow, CUDA driver), 또는 사내 표준 base image를 그대로 쓰고 싶을 때 이쪽이 답이다. 단, 이미지 크기가 크면 첫 콜드스타트가 zip 패키지보다 길어지는 경향이 있고, ECR push 파이프라인을 따로 운영해야 한다.

Rust 함수 한 개를 띄우고 싶다고 치자. 두 길 모두 가능하다. provided.al2023 위에 cargo lambda로 빌드해 zip을 올리거나, Dockerfile로 컨테이너 이미지를 만들어 ECR push에 올리거나. 빌드 캐시가 깨끗한 작은 함수면 zip이 콜드스타트가 빨라 유리하고, 의존성이 무거우면 컨테이너 쪽이 빌드·CI 통합 측면에서 더 단순하다. 같은 코드인데 운영 비용 분포가 다르다.

관리형이 숨기는 비용: 네 가지 부담

코드만 가져왔다는 말은 다른 모든 것을 AWS에 맡겼다는 뜻인데, 그 위탁이 청구서가 아닌 다른 영역으로 옮겨가는 부담이 네 갈래로 들어온다.

첫째, IAM Execution Role. Lambda는 EC2의 Instance Profile과 같은 형태로 함수 자격을 받지만, 매 함수마다 별도 role을 만들고 최소 권한을 직접 짜야 한다. EC2처럼 한 인스턴스 위에 IAM 한 번 붙여 여러 프로세스가 공유하는 게 아니다. 함수가 100개면 role이 100개, 또는 잘 짜인 한 묶음이 같이 자란다. 디테일은 실행 역할과 최소 권한: Lambda의 IAM에서 다룬다.

둘째, 콜드스타트. AWS는 첫 invoke 페널티를 그대로 사용자 응답 시간에 얹는다. 분당 10회 워크로드는 거의 안 보이지만, 트래픽이 급증하는 마케팅 캠페인이나 cron 일괄 처리 시점에 burst 단위로 다시 살아난다. SnapStart, Provisioned Concurrency, warm-up scheduler 같은 도구로 완화하지만 0으로 만들지는 못한다.

셋째, VPC connectivity. Lambda를 RDS·ElastiCache 같은 Private 리소스에 닿게 하려면 함수를 VPC 안에 넣어야 하는데, 그 순간 NAT Gateway 비용 또는 VPC Endpoint 비용이 따라 들어온다. 외부 API를 호출할 때마다 AWS는 NAT GW 한 시간당과 GB당 요금을 양쪽으로 부과한다. 디테일은 Lambda와 VPC: Private 리소스에 접근하는 비용에서 다룬다.

넷째, 관측. Lambda는 @requestId / @duration / @billedDuration / @memorySize 같은 자동 필드를 CloudWatch Logs에 푸시하지만, 분산 트레이싱(다른 함수와 DynamoDB 호출 사이의 latency 분포), 커스텀 메트릭(business KPI percentile), 호출별 인풋·아웃풋 페이로드는 X-Ray, Powertools for Lambda, 또는 EMF로 직접 짜 넣어야 한다. 코드만 가져왔지만 관측 코드는 따로 가져와야 하는 셈이다.

처음 Lambda를 띄울 때 hello world가 5분 안에 도는데 운영용 함수는 일주일째 안 끝나는 경험을 자주 한다. IAM role이 너무 좁아 DynamoDB 호출이 AccessDenied로 막히거나, VPC 안에서 NAT GW 비용이 한 달 만에 함수 단가의 10배가 되거나, 첫 사용자가 콜드스타트 3초를 그대로 보거나, 디버깅하려고 보니 로그에 페이로드가 안 찍혀 있거나. 이 네 가지를 모두 점검하고 나서야 Lambda를 운영 코드로 부를 수 있다.

다음 편으로

첫 함수를 띄울 때 가장 먼저 손대야 할 손잡이 한 줄을 꼽으라면 Memory다. 기본 128MB가 거의 모든 운영 워크로드에 부족하다고 잘라 말하긴 어렵지만, Lambda의 vCPU는 메모리에 비례해 자동으로 따라 오르므로 메모리를 256MB·512MB로 올리면 CPU도 같이 올라가 같은 코드가 더 빨리 끝나는 경우가 있다. 그래서 GB-초 청구가 도리어 줄어드는 경우도 있고, 이 맞바꿈은 값을 바꿔 가며 직접 재 봐야 한다. 1,769MB가 한 vCPU 풀 등가 지점, 그 위로는 6 vCPU까지 단계적으로 늘어나는 메모리·CPU 매핑 한 줄을 들고 시작하면 좋다.

다음 편에서는 핸들러 함수가 이벤트 한 번에 어떻게 진입하는지, 트리거 종류(API Gateway / S3 / EventBridge / SQS)에 따라 event 객체 구조가 어떻게 달라지는지, context 객체에 무엇이 들어 있는지를 다룬다.

참고 자료

YouTube 영상

채널 보기
투영과 예측, 그리고 선형 결합 | 선형대수학
행렬의 가장 중요한 연산 - 행렬 곱셈 | 선형대수학
직교성과 벡터 투영 | 선형대수학
AI는 데이터를 어떻게 분류할까? 벡터의 거리와 KNN 알고리즘 | 선형대수학
트라이(Trie)에서 단어를 삭제하는 방법 | Trie 자료구조 이야기
내적의 기하학적 의미와 코사인 유사도 원리 | 선형대수학
AI를 위한 선형대수학 - 소개 | 선형대수학
마지막편, 트라이 노드를 50% 이상 줄이는 방법? 압축 트라이 성능 분석 | Trie 자료구조 이야기