기준: DVA-C02 시험 가이드오류 제보
인프라를 코드로 정의한다는 생각의 역사는 길다. 처음엔 셸 스크립트로 서버를 설정했다. 그다음엔 Chef·Puppet 같은 구성 관리 도구가 "원하는 상태를 선언하면 맞춰준다"는 발상을 가져왔다. AWS는 CloudFormation으로 "클라우드 자원 전체를 선언적 템플릿으로 묶는다"는 모델을 정착시켰다.
그런데 YAML/JSON 템플릿에는 근본적 한계가 있다. 템플릿은 데이터지 프로그램이 아니라서 반복문도, 조건문도, 타입 검사도, 함수 추상화도 없다. 1,000줄짜리 템플릿을 복사·붙여넣기로 관리하다 보면 "차라리 코드였으면" 싶어진다. AWS CDK(Cloud Development Kit)는 그 요구에 대한 답이다. 진짜 프로그래밍 언어로 인프라를 쓰되, 결과물은 CloudFormation으로 떨어뜨린다.
DVA-C02에서 CDK 자체는 깊게 다루지 않는다(synth가 CloudFormation을 만든다, bootstrap은 계정·리전당 1회 정도). 비중이 큰 쪽은 그 주변의 "서버리스 아키텍처를 어떻게 조립하느냐"다. Lambda를 Lambda가 직접 호출하면 왜 안 되는지, SQS와 SNS와 EventBridge가 언제 갈리는지, 멱등성을 왜 보장해야 하는지가 여기에 든다.
이번 글은 세 가지를 다룬다.
인프라를 다루는 방식은 추상화 수준이 점점 올라온 역사다. 이 사다리를 이해하면 CDK가 왜 "맨 위 칸"인지 보인다.
CDK의 핵심 통찰은 "선언적 결과물의 안전함은 유지하되, 그것을 만드는 과정은 명령적 언어의 표현력으로 한다"는 분리다. 즉 개발자는 TypeScript로 for 루프를 돌려 서브넷 10개를 만들 수 있지만, 최종적으로 배포되는 건 CloudFormation이 검증하고 롤백할 수 있는 선언적 템플릿이다.
이 구도는 컴파일러의 소스 코드 → 중간 표현(IR) → 기계어 파이프라인과 정확히 닮았다. CDK 코드는 "소스 코드"이고, cdk synth가 만드는 CloudFormation 템플릿은 "중간 표현(IR)"이며, CloudFormation 엔진이 실제 API를 호출해 자원을 만드는 게 "기계어 실행"이다. 컴파일러가 고수준 언어의 표현력과 저수준 코드의 실행 가능성을 분리하듯, CDK는 고수준 언어의 추상화와 CloudFormation의 안전한 배포 모델을 분리한다. 그래서 CDK로 짠 코드의 버그는 대부분 synth 단계(템플릿 생성)에서 잡히지, 실제 자원이 반쯤 만들어진 뒤에 터지지 않는다 — 컴파일 타임에 잡히는 타입 에러와 같은 이치다.
CDK의 모든 것은 Construct라는 단위다. 이건 객체지향의 컴포지션을 인프라에 그대로 가져온 것이다. Construct는 세 층위(L1/L2/L3)로 나뉘는데, 이 추상화 단계를 이해하는 것이 CDK의 출발점이다.
| 층위 | 이름 | 의미 | 예 |
|---|---|---|---|
| L1 | CFN Resources | CloudFormation 자원과 1:1 대응(Cfn 접두사) | CfnBucket — 모든 속성을 직접 지정 |
| L2 | Curated Constructs | 합리적 기본값과 편의 메서드를 입힌 추상화 | s3.Bucket — 암호화·버저닝을 메서드로 |
| L3 | Patterns | 여러 자원을 하나의 패턴으로 묶음 | ApplicationLoadBalancedFargateService |
L1은 CloudFormation을 거의 그대로 노출하므로 표현력은 최대지만 장황하다. L2는 table.grant_read_write_data(handler) 한 줄로 "이 Lambda가 이 테이블을 읽고 쓰도록 IAM 정책을 자동 생성"해준다 — 손으로 IAM 정책 JSON을 쓰던 고통을 메서드 호출로 바꾼다. L3은 "ALB + Fargate 서비스 + 타깃 그룹 + 보안 그룹"을 통째로 한 객체로 만든다.
⚠️ 함정: L2 Construct의
grant_*메서드가 "최소 권한 IAM 정책을 자동 생성"한다는 점은 양날의 검이다. 편하지만, synth된 템플릿을 한 번도 안 보면 어떤 권한이 실제로 부여됐는지 모른 채 배포하게 된다. 시험 맥락에선 "CDK가 IAM을 자동 생성한다"는 사실 자체보다, 그 자동 생성이cdk synth로 만들어진 CloudFormation 템플릿에 박제된다는 점이 중요하다 — CDK는 런타임에 권한을 주는 게 아니라, 결국 CloudFormation 자원으로 변환되어 배포된다.
CDK 워크플로에서 시험이 묻는 명령은 사실상 두 개로 압축된다.
cdk bootstrap # 계정·리전당 1회: CDK가 쓸 S3 버킷·ECR·IAM 역할을 깔아둠
cdk synth # CDK 코드 → CloudFormation 템플릿 (배포 안 함)
cdk diff # 현재 배포된 스택과 새 템플릿의 차이
cdk deploy # synth + CloudFormation 배포
cdk destroy # 스택 삭제cdk synth는 "코드를 실행해서 CloudFormation 템플릿(JSON)을 표준 출력으로 뱉는" 단계다. 이 단계에서는 AWS에 아무것도 배포되지 않는다 — 순수하게 로컬에서 코드가 돌아 템플릿이 만들어진다. cdk deploy는 그 템플릿을 CloudFormation에 올려 실제 자원을 만든다.
cdk bootstrap은 헷갈리기 쉬운 지점이다. CDK는 배포 과정에서 자산(asset) — 예를 들어 Lambda 코드 zip, Docker 이미지 — 을 어딘가 올려둬야 CloudFormation이 그걸 참조할 수 있다. bootstrap은 그 "어딘가"인 S3 버킷, ECR 리포지토리, 배포용 IAM 역할을 미리 깔아두는 일회성 준비다. 계정과 리전의 조합마다 한 번씩 해야 한다.
세 도구는 모두 최종적으로 CloudFormation으로 귀결된다. 차이는 "얼마나 추상화했고, 무엇에 특화됐느냐"다.
| 도구 | 입력 | 특화 | 최종 변환 | 누구에게 |
|---|---|---|---|---|
| CloudFormation | YAML/JSON 템플릿 | 범용, 모든 AWS 자원 | (자기 자신) | 가장 안정적인 표준이 필요할 때 |
| SAM | YAML(매크로) | 서버리스(Lambda/API/DynamoDB) | CloudFormation(Transform 확장) | 서버리스를 간결하게 |
| CDK | 프로그래밍 언어 | 범용 + 높은 추상화 | CloudFormation(synth) | 코드의 표현력·재사용이 필요할 때 |
SAM은 사실 CloudFormation의 매크로(macro) 다 — 템플릿 맨 위 Transform: AWS::Serverless-2016-10-31 한 줄이 "이 짧은 SAM 문법을 펼쳐 긴 CloudFormation으로 바꿔달라"는 지시다. AWS::Serverless::Function 한 블록이 배포 시 Lambda 함수 + 실행 역할 + 로그 그룹 + (필요시) API Gateway 자원으로 전개된다. CDK가 코드를 펼쳐 템플릿을 만든다면, SAM은 짧은 YAML을 펼쳐 긴 YAML을 만든다 — 둘 다 "압축된 표현을 CloudFormation으로 푸는" 일이다.
그렇다면 셋 중 무엇을 골라야 하나? 실무 휴리스틱은 이렇다. 팀이 YAML에 익숙하고 순수 서버리스(Lambda+API+DDB)면 SAM이 가장 군더더기 없다. 복잡한 인프라(VPC, ECS, 여러 환경의 반복 생성)에 프로그래밍 로직이 필요하면 CDK다. 조직 표준이 이미 CloudFormation이고 외부 도구 도입이 어렵다면 CloudFormation 그대로다. 멀티 클라우드까지 가야 하면 Terraform/Pulumi가 후보지만 이건 AWS 시험 범위 밖이다. DVA 관점에선 "SAM=서버리스 특화, CDK=프로그래밍, 둘 다 CloudFormation으로 귀결"만 확실히 잡으면 된다.
서버리스 아키텍처의 대표적인 안티패턴은 "Lambda A가 Lambda B를 동기로 직접 호출"하는 것이다. 왜 나쁜지를 표면적 이유("비용 2배")만 외우면 변형 문제에서 흔들린다. 근본 원인은 다음과 같다.
나쁨: [API GW] → [Lambda A] ──동기 invoke──> [Lambda B]
좋음: [API GW] → [Lambda A] ──> [SQS] ──> [Lambda B]
동기 호출(InvocationType: RequestResponse)에서 Lambda A는 Lambda B가 끝날 때까지 블로킹한 채 기다린다. 이때 일어나는 일:
SQS를 사이에 끼우면 이 모든 게 풀린다. A는 메시지를 큐에 넣고 즉시 반환하므로 기다리지 않는다(비용·타임아웃 해결). B는 자기 속도로 큐를 비우므로 독립적으로 확장된다(결합·확장 해결). B가 잠시 죽어도 메시지는 큐에 쌓여 있다가 B가 살아나면 처리된다(내결함성).
이 "큐를 사이에 끼워 생산자와 소비자를 떼어놓는" 것은 분산 시스템의 고전 생산자-소비자 문제(producer-consumer) 와 그 해법인 유계 버퍼(bounded buffer) 다. 생산 속도와 소비 속도가 다를 때, 둘을 직접 연결하면 빠른 쪽이 느린 쪽을 압도하거나 느린 쪽이 빠른 쪽을 막는다. 사이에 버퍼(큐)를 두면 둘은 서로의 속도를 몰라도 되고, 버퍼가 속도 차를 흡수한다. SQS가 바로 그 버퍼다. 그리고 큐가 메시지를 잠시 쌓아두는 능력이 곧 백프레셔(backpressure) — 다운스트림이 못 따라올 때 업스트림을 강제로 막지 않고 일감을 잠시 보관하는 — 의 토대다.
"Lambda를 직접 부르지 말고 비동기로"까지는 외우기 쉽지만, 그 비동기 매개가 SQS냐 SNS냐 EventBridge냐는 시나리오마다 다르다. 이 셋의 메시징 모델 차이를 묻는 문제가 있다.
| 서비스 | 모델 | 핵심 성질 | 전형적 시나리오 |
|---|---|---|---|
| SQS | 큐(1:1) | 한 메시지를 한 소비자가 가져가 처리 후 삭제. 버퍼링·재시도·DLQ | 작업 큐, 부하 평탄화, 백프레셔 |
| SNS | 펍/섭(1:N) | 한 메시지를 여러 구독자에게 동시 푸시(팬아웃) | 한 이벤트로 여러 시스템 동시 통지 |
| EventBridge | 이벤트 버스(라우팅) | 내용 기반 규칙으로 이벤트를 라우팅. SaaS·스케줄 통합 | 이벤트 라우팅, 스케줄, 서드파티 통합 |
갈림길은 "한 메시지를 한 곳이 처리하느냐, 여러 곳이 받느냐"다. 작업을 나눠 처리하려면 SQS(한 메시지=한 소비자), 한 사건을 여러 시스템에 동시에 알리려면 SNS(팬아웃)다. 그리고 둘을 결합한 SNS → 여러 SQS 패턴(fan-out)이 있다. SNS가 여러 SQS로 뿌리고, 각 SQS가 자기 소비자에게 버퍼·재시도를 제공한다. EventBridge는 한 단계 위로, "이벤트의 내용을 보고 규칙에 따라 다른 대상으로 보내는" 라우팅과 cron 스케줄, 서드파티 SaaS 이벤트 수신까지 맡는다.
SNS의 팬아웃에 SQS를 끼우는 이유가 미묘하다. SNS가 Lambda를 직접 구독할 수도 있는데 왜 SNS→SQS→Lambda를 권할까? SNS가 Lambda를 직접 호출하면, Lambda가 처리 실패 시 SNS의 재시도 정책에 의존해야 하고, 일시적 폭주를 흡수할 버퍼가 없다. 사이에 SQS를 두면 (1) 메시지가 큐에 안전히 보관돼 소비자 장애에도 유실되지 않고, (2) 소비자가 자기 속도로 처리하며, (3) 처리 실패 메시지를 DLQ로 보내 나중에 분석할 수 있다. "SNS는 뿌리는 데, SQS는 받아 버티는 데" 강하므로 둘을 합치는 것이다.
서버리스 메시징의 거의 모든 시험 함정 뒤에는 at-least-once delivery(최소 한 번 전달) 가 깔려 있다. SQS 표준 큐, SNS, Lambda 비동기 호출은 모두 "메시지를 최소 한 번 전달"을 보장하지, "정확히 한 번"을 보장하지 않는다. 즉 같은 메시지가 두 번 이상 올 수 있다.
왜 그럴까? 분산 시스템에서 "정확히 한 번"은 사실상 불가능에 가깝다. 메시지를 보내고 소비자가 "처리 완료" 응답을 보냈는데 그 응답이 네트워크에서 유실되면, 발신자는 "전달 실패"로 보고 다시 보낸다 — 소비자 입장에선 같은 메시지가 두 번 온다. 이 모호함을 없애려면 무한정 대기하거나 막대한 합의 비용을 치러야 하므로, 실용 시스템은 "최소 한 번 + 소비자가 중복을 견딘다"를 택한다.
그래서 소비자는 멱등(idempotent) 해야 한다 — 같은 요청을 몇 번 처리해도 결과가 한 번 처리한 것과 같아야 한다. 구현은 보통 이렇다.
attribute_not_exists(id) 조건으로, 그 ID가 없을 때만 쓴다. 두 번째 시도는 조건 실패로 무시된다."정확히 한 번"이 어려운 건 분산 시스템 이론의 두 장군 문제(Two Generals' Problem) 와 직결된다 — 신뢰할 수 없는 네트워크 위에서 두 당사자가 "메시지가 확실히 전달됐다"는 공통 인식(common knowledge)에 도달하는 것은 불가능함이 증명돼 있다. ACK가 유실될 수 있는 한, 발신자는 "받았는지" 영원히 확신할 수 없으므로 재전송을 택하고, 그 결과 중복이 생긴다. 그래서 실무의 정답은 "정확히 한 번 전달"을 시스템에 강요하는 게 아니라, "최소 한 번 전달 + 멱등한 처리"로 정확히 한 번의 효과(exactly-once processing) 를 애플리케이션 레벨에서 만드는 것이다. SQS·SNS·EventBridge가 한결같이 멱등성을 권하는 이유다.
⚠️ 함정: SQS 표준 큐는 at-least-once이고 순서 보장도 best-effort(거의 순서대로지만 보장 X)다. FIFO 큐만 "정확히 한 번 처리(exactly-once processing)"와 엄격한 순서를 제공한다 — 단, 처리량이 표준보다 낮고(초당 300 메시지, 배치로 3,000) 비용·제약이 있다. 시험에서 "순서가 중요하다/중복을 절대 막아야 한다"면 FIFO, "높은 처리량이 우선이고 중복은 앱이 멱등성으로 처리한다"면 표준 큐다. "표준 큐인데 순서가 보장된다"는 선택지는 함정이다.
위 원칙들 외에 시험이 "잘 설계된 아키텍처"를 물을 때 깔리는 토대를 정리한다. 대부분 AWS Well-Architected Framework의 여섯 기둥(Operational Excellence, Security, Reliability, Performance Efficiency, Cost Optimization, Sustainability)으로 환원된다.
"작은 함수 여러 개"가 항상 옳은 건 아니다. 함수를 잘게 쪼갤수록 콜드 스타트 표면이 늘고, 함수 간 호출 지연·복잡도가 커진다(이른바 "Lambda 핀볼" 안티패턴 — 요청이 함수 사이를 핀볼처럼 튕겨 다니는 것). 그래서 실무는 "단일 책임이되 의미 있는 응집 단위"로 묶는다 — 한 도메인의 CRUD를 하나의 함수로 두고 내부에서 라우팅하는 "Lambdalith(람다 모놀리스)" 절충도 흔하다. 시험은 "Lambda→Lambda 동기 호출 금지" 같은 명확한 안티패턴을 다룬다. 다만 "무조건 잘게 쪼갠다"가 정답이 아니라는 감각은 필요하다.
CDK를 관통하는 한 문장은 "프로그래밍 언어의 표현력으로 인프라를 쓰되, 결과는 CloudFormation의 안전한 선언적 모델로 합성한다"이다. 컴파일러가 고수준 언어와 기계어를 잇듯, CDK는 코드와 CloudFormation을 잇는다.
synth는 코드를 템플릿으로 펴는 컴파일이고, bootstrap은 그 배포를 가능하게 하는 일회성 발판이다.다음 글에서는 Week 12 전체 — 컨테이너와 IaC — 를 종합 복습하며 실전 시나리오 문제로 마무리한다.
선택지를 클릭하면 정답·해설이 펼쳐집니다.
문제 1
CDK 프로젝트를 새 계정·리전에 처음 배포하려는데 "bootstrap" 관련 오류가 난다. cdk bootstrap이 하는 일과 실행 빈도로 옳은 것은?
문제 2
cdk synth와 cdk deploy의 차이로 옳은 것은?
문제 3
API Gateway 뒤의 Lambda A가 작업의 일부를 Lambda B에 넘기려 한다. 비용·결합도·내결함성 측면에서 권장되는 구성은?
문제 4
하나의 주문 완료 이벤트가 발생하면 (1) 재고 시스템, (2) 이메일 발송, (3) 분석 파이프라인이 동시에 그 이벤트를 받아 각자 처리해야 한다. 가장 적합한 구성은?
문제 5
SQS 표준 큐로 메시지를 받는 Lambda 소비자가, 같은 메시지를 가끔 두 번 처리해 중복 레코드가 생긴다. 근본 원인과 올바른 대응은?
문제 6
SAM 템플릿과 CDK가 공통으로 가지는 성질로 옳은 것은?
문제 7
지연에 민감한 동기 REST API의 Lambda가 첫 호출에서 콜드 스타트로 느리다. 콜드 스타트를 줄이는 방법으로 적절하지 않은 것은?