CertNote/ 출퇴근 학습 노트
로드맵
KOEN
CLF-C02 · FoundationalCloud Practitioner - Foundational
DVA-C02 · AssociateDeveloper - Associate
  • Week 1
    • 1.개발자 시점에서 본 AWS: 리전, 인프라, 그리고 책임의 경계
    • 2.IAM의 4대 엔터티: User, Group, Role, Policy
    • 3.STS와 정책 조건: 임시 자격증명의 깊은 세계
    • 4.AWS CLI와 SDK: 자격증명 체이닝부터 SigV4까지
    • 5.Week 1 종합: 인프라와 IAM이 만드는 신뢰의 사슬
  • Week 2
  • Week 3
  • Week 4
    • 1.API Gateway REST API: 요청이 백엔드에 도달하기까지의 전체 경로
    • 2.API Gateway 통합 유형과 VTL 매핑 템플릿: Lambda 없이 DynamoDB를 직접 호출하기
    • 3.API Gateway 보안·캐싱·스로틀링: SigV4부터 Token Bucket까지 요청이 통제되는 방법
    • 4.WebSocket API와 HTTP API: 실시간 연결과 경량 프록시의 내부 동작
    • 5.Week 4 복습: API Gateway 종합 시나리오로 실전 감각 다지기
  • Week 5
  • Week 6
  • Week 7
  • Week 8
  • Week 9
  • Week 10
  • Week 11
  • Week 12
  • Week 13
SAA-C03 · AssociateSolutions Architect - Associate
SOA-C02 · AssociateCloudOps Engineer - Associate
SAP-C02 · ProfessionalSolutions Architect - Professional
DOP-C02 · ProfessionalDevOps Engineer - Professional
SCS-C03 · SpecialtySecurity - Specialty
MLA-C01 · AssociateMachine Learning Engineer - Associate
AIF-C01 · FoundationalAI Practitioner - Foundational
DEA-C01 · AssociateData Engineer - Associate
MLS-C01 · SpecialtyMachine Learning - Specialty
합격 후기

© CertNote · 주간랩스

  • 소개
  • 수정 기록
  • 문의
  • 이용약관
  • 개인정보처리방침
← DVA-C02/Week 4/Day 2

기준: DVA-C02 시험 가이드·오류 제보

DVA-C02· AssociateWeek 4 · Day 2읽기 약 11분

Day 2 - API Gateway 통합 유형과 VTL 매핑 템플릿: Lambda 없이 DynamoDB를 직접 호출하기

API Gateway가 단순히 "Lambda의 HTTP 프록시"라고 생각한다면 절반만 아는 것이다. API Gateway는 Lambda 없이도 DynamoDB, SQS, Kinesis, S3 같은 AWS 서비스를 직접 호출할 수 있다. 이 패턴이 실무에서 의미 있는 이유는 명확하다 — Lambda를 거치지 않으면 지연 시간이 줄고, 비용이 줄고, 관리할 코드가 없어진다.

오늘은 API Gateway의 다섯 가지 통합 유형을 해부하고, VTL(Velocity Template Language) 매핑 템플릿으로 요청·응답을 변환하는 방법을 파고든다. Lambda Proxy vs non-Proxy의 차이를 코드 레벨에서 이해하면 "왜 CORS가 안 되지?", "왜 응답 형식이 깨지지?" 같은 실무 문제가 즉각 해결된다.

다섯 가지 통합 유형의 결정 트리

백엔드가 필요한가?
    │
    ├── 아니오 (개발/테스트 단계) → MOCK 통합
    │
    └── 예
         │
         ├── Lambda 함수?
         │    ├── 변환 없이 그대로 전달 → AWS_PROXY (Lambda Proxy)
         │    └── VTL로 변환 필요 → AWS (Lambda non-Proxy)
         │
         ├── 외부 HTTP 엔드포인트?
         │    ├── 그대로 전달 → HTTP_PROXY
         │    └── VTL로 변환 → HTTP
         │
         └── AWS 서비스 직접?
              └── AWS (Service Integration) + VTL 매핑

AWS_PROXY (Lambda Proxy): 기본 통합으로 쓰는 이유

Lambda Proxy 통합은 API Gateway가 요청을 변환하지 않고 Lambda로 그대로 전달한다. 대신 Lambda가 완전한 HTTP 응답 형식을 반환해야 한다.

Lambda가 받는 이벤트 구조:

{
  "version": "1.0",
  "resource": "/products/{productId}",
  "path": "/products/P001",
  "httpMethod": "GET",
  "headers": {
    "Authorization": "Bearer eyJhbGciOiJSUzI1NiJ9...",
    "Content-Type": "application/json",
    "User-Agent": "Mozilla/5.0..."
  },
  "queryStringParameters": {
    "includeStock": "true",
    "lang": "ko"
  },
  "pathParameters": {
    "productId": "P001"
  },
  "stageVariables": {
    "env": "prod",
    "lambdaAlias": "prod"
  },
  "requestContext": {
    "accountId": "123456789",
    "apiId": "abc123",
    "stage": "prod",
    "requestId": "req-abc",
    "identity": {
      "sourceIp": "203.0.113.1",
      "userAgent": "Mozilla/5.0..."
    },
    "authorizer": {
      "userId": "U001",
      "scope": "read:products",
      "claims": {
        "sub": "auth0|xxx",
        "email": "user@example.com"
      }
    }
  },
  "body": null,
  "isBase64Encoded": false
}

Lambda가 반환해야 하는 응답 구조:

def lambda_handler(event, context):
    http_method = event['httpMethod']
    path_params = event.get('pathParameters', {}) or {}
    query_params = event.get('queryStringParameters', {}) or {}
    
    # body는 항상 문자열 — JSON이면 직접 파싱
    body = json.loads(event.get('body') or '{}')
    
    # requestContext.authorizer에서 인증 정보 추출
    user_id = event['requestContext']['authorizer']['userId']
    
    return {
        'statusCode': 200,  # 필수
        'headers': {        # 선택 (CORS를 위해 거의 필수)
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*',
            'X-Request-Id': event['requestContext']['requestId']
        },
        'multiValueHeaders': {  # 선택 (하나의 헤더 키에 여러 값)
            'Set-Cookie': ['session=abc', 'csrf=xyz']
        },
        'body': json.dumps({   # 필수 (반드시 문자열)
            'productId': path_params.get('productId'),
            'name': '상품명'
        }, ensure_ascii=False),
        'isBase64Encoded': False  # 바이너리 응답이면 True
    }

⚠️ 함정: Lambda Proxy에서 body는 항상 문자열이다. 객체를 그대로 반환하면 Lambda가 직렬화하지 못해 에러가 난다. json.dumps(data)로 문자열화하거나, 반대로 이벤트에서 받은 body를 사용할 때는 json.loads(event['body'])로 파싱해야 한다. event['queryStringParameters']가 쿼리 파라미터가 없을 때 None이 될 수 있으므로 or {}로 안전하게 처리한다.

AWS (Lambda Non-Proxy): VTL로 요청과 응답을 변환

non-Proxy 통합은 API Gateway가 VTL(Velocity Template Language)로 요청을 변환한 후 Lambda를 호출하고, Lambda의 응답도 VTL로 변환해 클라이언트에 돌려준다. Lambda Proxy보다 복잡하지만 Lambda 코드를 단순하게 유지할 수 있다.

실제 코드는 이벤트 구조를 알 필요 없이 비즈니스 로직만 담으면 되고, API Gateway가 입출력을 담당한다.

통합 요청 VTL (API GW → Lambda):

## 경로 파라미터와 쿼리 파라미터를 Lambda가 원하는 형식으로 변환
{
  "productId": "$input.params('productId')",
  "includeStock": "$input.params('includeStock')" == "true",
  "userId": "$context.authorizer.userId",
  "requestId": "$context.requestId"
}

Lambda 코드 (단순화):

def lambda_handler(event, context):
    # 이제 이벤트는 API Gateway 구조가 아닌 VTL이 만든 단순 구조
    product_id = event['productId']
    include_stock = event['includeStock']
    
    product = get_product(product_id)
    if include_stock:
        product['stock'] = get_stock(product_id)
    
    return product  # 딕셔너리 그대로 반환 (statusCode 필요 없음)

통합 응답 VTL (Lambda 응답 → API GW):

## Lambda가 반환한 JSON을 클라이언트용으로 변환
{
  "id": "$input.path('$.productId')",
  "name": "$input.path('$.name')",
  "price": $input.path('$.price'),
  #if($input.path('$.stock') != '')
  "stockAvailable": $input.path('$.stock')
  #end
}

Integration Response에서 HTTP 상태 코드 매핑:

Lambda 반환값의 errorType 필드에 정규식 매칭:
".*NotFound.*"   → 404
".*Unauthorized.*" → 403
".*BadRequest.*"   → 400
(default)          → 200

VTL(Velocity Template Language)은 Apache Velocity 프로젝트에서 유래한 Java 웹 개발용 템플릿 엔진이다. 구문이 단순하고 로직 표현이 가능해 API Gateway가 채택했다. 실무에서 VTL을 새로 작성하는 경우는 줄고 있지만, 시험에서는 여전히 핵심 개념이다.

AWS 서비스 직접 통합: Lambda 없이 DynamoDB CRUD

API Gateway → DynamoDB 직접 통합의 실제 구현을 보면 VTL의 강력함을 알 수 있다.

GET /orders/{orderId} → DynamoDB GetItem:

통합 요청 설정:

  • Type: AWS
  • Service: DynamoDB
  • Action: GetItem
  • HTTP Method: POST (DynamoDB API는 모두 POST)

통합 요청 VTL:

{
  "TableName": "Orders",
  "Key": {
    "orderId": {
      "S": "$input.params('orderId')"
    }
  }
}

DynamoDB 응답(원시 형식):

{
  "Item": {
    "orderId": {"S": "O001"},
    "status": {"S": "DELIVERED"},
    "total": {"N": "59900"},
    "items": {"L": [{"M": {"sku": {"S": "SKU001"}, "qty": {"N": "2"}}}]}
  }
}

통합 응답 VTL (DynamoDB 형식 → 깔끔한 JSON):

#set($item = $input.path('$.Item'))
{
  "orderId": "$item.orderId.S",
  "status": "$item.status.S",
  "total": $item.total.N,
  "items": [
    #foreach($i in $item.items.L)
    {
      "sku": "$i.M.sku.S",
      "quantity": $i.M.qty.N
    }#if($foreach.hasNext),#end
    #end
  ]
}
# API Gateway → DynamoDB 직접 통합 설정
aws apigateway put-integration \
  --rest-api-id abc123 \
  --resource-id res001 \
  --http-method GET \
  --type AWS \
  --integration-http-method POST \
  --uri "arn:aws:apigateway:ap-northeast-2:dynamodb:action/GetItem" \
  --credentials "arn:aws:iam::123:role/APIGatewayDynamoDBRole" \
  --request-templates 'application/json={"TableName":"Orders","Key":{"orderId":{"S":"$input.params('"'"'orderId'"'"')"}}}'

단순 GetItem/PutItem 패턴에서는 Lambda가 불필요한 중간 레이어가 된다. API Gateway → DynamoDB 직접 통합으로 Lambda를 제거하면 호출 지연이 줄고 Lambda 비용이 사라진다. 복잡한 비즈니스 로직이 필요한 엔드포인트는 여전히 Lambda를 쓴다.

Gateway Response: API Gateway 자체 에러의 커스터마이징

API Gateway가 자체적으로 생성하는 에러(Lambda를 호출하기 전에 발생하는 인증 실패, 스로틀링, 잘못된 요청 등)를 커스터마이징한다.

에러 타입기본 발생 상황
UNAUTHORIZEDLambda Authorizer가 401 반환
ACCESS_DENIEDIAM 인증 실패
THROTTLED스로틀링 한도 초과
MISSING_AUTHENTICATION_TOKEN잘못된 경로/메서드
DEFAULT_4XX모든 4XX 기본 응답
DEFAULT_5XX모든 5XX 기본 응답
# 401 응답에 CORS 헤더 추가 (Lambda Proxy에서 CORS 헤더가 없는 에러에 대응)
aws apigateway put-gateway-response \
  --rest-api-id abc123 \
  --response-type UNAUTHORIZED \
  --response-parameters '{"gatewayresponse.header.Access-Control-Allow-Origin": "'"'"'*'"'"'"}'  \
  --response-templates '{"application/json": "{\"message\": \"인증이 필요합니다\", \"code\": \"UNAUTHORIZED\"}"}'

⚠️ 함정: Lambda Proxy + REST API 조합에서 CORS 문제가 흔하다. 정상 응답은 Lambda가 Access-Control-Allow-Origin 헤더를 반환하므로 잘 된다. 하지만 API Gateway 자체에서 에러가 발생할 때(Lambda Authorizer가 거부하거나, 스로틀링될 때) API Gateway가 반환하는 에러에는 CORS 헤더가 없다. 브라우저는 CORS 헤더가 없는 에러를 "CORS 에러"로 표시한다. Gateway Response를 설정하면 API Gateway 자체 에러에도 CORS 헤더를 붙일 수 있다.

VTL 핵심 변수와 함수

변수설명예시
$input.body요청 본문 원본 문자열{"name":"test"}
$input.json('$.field')JSONPath로 필드 추출$input.json('$.name')
$input.params('name')경로/헤더/쿼리 파라미터$input.params('userId')
$input.path('$.key')응답 JSON에서 값 추출$input.path('$.Items')
$context.requestId요청 IDabc-123-def
$context.identity.sourceIp클라이언트 IP203.0.113.1
$context.authorizer.userIdLambda Authorizer 반환 값U001
$context.authorizer.claims.subCognito JWT sub`auth0
$stageVariables.varName스테이지 변수prod
$util.escapeJavaScript(str)JS 이스케이프XSS 방지
$util.base64Encode(str)Base64 인코딩바이너리 처리
$util.parseJson(str)JSON 파싱중첩 JSON 처리

VTL 제어 구문:

## if/elseif/else
#if($input.params('filter') == 'active')
  "status": "ACTIVE"
#elseif($input.params('filter') == 'inactive')
  "status": "INACTIVE"
#else
  "status": "ALL"
#end
 
## foreach 반복
#foreach($item in $items)
  {"id": "$item.id"}#if($foreach.hasNext),#end
#end
 
## 변수 설정
#set($tableName = "Orders-$stageVariables.env")

CORS 처리: 통합 방식별 올바른 구현

CORS는 통합 방식에 따라 처리 방법이 다르다.

통합 방식OPTIONS preflightCORS 헤더 위치
HTTP APIAPI GW 자동 처리API GW 설정에서
REST API + Lambda Proxy수동 OPTIONS 메서드 + MockLambda 응답에 포함
REST API + non-Proxy콘솔 "Enable CORS" 버튼API GW Integration Response

REST API + Lambda Proxy에서 완전한 CORS 구현:

# Lambda에서 모든 응답에 CORS 헤더 포함
def cors_headers():
    return {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': 'https://myapp.com',  # * 또는 특정 도메인
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Api-Key',
        'Access-Control-Max-Age': '86400'  # preflight 캐시 시간 (초)
    }
 
def lambda_handler(event, context):
    # OPTIONS 요청은 API Gateway가 Mock으로 처리하거나 Lambda에서 직접 처리
    if event['httpMethod'] == 'OPTIONS':
        return {'statusCode': 200, 'headers': cors_headers(), 'body': ''}
    
    # 실제 요청 처리
    result = process_request(event)
    
    return {
        'statusCode': 200,
        'headers': cors_headers(),
        'body': json.dumps(result)
    }

Binary Media Types: 이미지·PDF 처리

API Gateway는 기본적으로 JSON을 처리한다. 이미지(JPEG, PNG), PDF 같은 바이너리를 처리하려면 Binary Media Types를 설정해야 한다.

# REST API에 Binary Media Types 등록
aws apigateway update-rest-api \
  --rest-api-id abc123 \
  --patch-operations \
    'op=add,path=/binaryMediaTypes/image~1jpeg' \
    'op=add,path=/binaryMediaTypes/image~1png' \
    'op=add,path=/binaryMediaTypes/application~1pdf'

Lambda에서 바이너리 응답:

import base64
 
def lambda_handler(event, context):
    # 파일 읽기
    with open('/tmp/image.jpg', 'rb') as f:
        image_data = f.read()
    
    return {
        'statusCode': 200,
        'headers': {'Content-Type': 'image/jpeg'},
        'body': base64.b64encode(image_data).decode('utf-8'),
        'isBase64Encoded': True  # API GW가 자동으로 디코드해서 전달
    }

API Gateway의 Content Negotiation은 HTTP 표준 Accept 헤더를 사용한다. 클라이언트가 Accept: image/jpeg를 보내고, Binary Media Types에 image/jpeg가 등록되어 있으면 API GW가 base64 디코딩해서 바이너리로 전달한다. Accept: application/json을 보내면 base64 인코딩된 문자열 그대로 전달한다. 이 협상이 잘못 설정되면 이미지가 깨져 보이는 문제가 발생한다.

요청 검증: Lambda 호출 전에 차단하기

요청 검증은 잘못된 요청을 Lambda 호출 전에 400으로 차단해 비용을 절감하고 백엔드를 보호한다.

검증 가능 항목:

  1. 경로 파라미터: 필수 파라미터 존재 여부
  2. 쿼리 파라미터: 필수 쿼리 파라미터 존재 여부
  3. 헤더: 필수 헤더 존재 여부
  4. 요청 본문: JSON Schema 기반 구조 검증
# 요청 검증기 생성 (본문 검증만)
aws apigateway create-request-validator \
  --rest-api-id abc123 \
  --name ValidateBodyOnly \
  --validate-request-body true \
  --validate-request-parameters false
 
# JSON Schema 모델 생성
aws apigateway create-model \
  --rest-api-id abc123 \
  --name CreateOrderRequest \
  --content-type 'application/json' \
  --schema '{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "CreateOrderRequest",
    "type": "object",
    "required": ["customerId", "items"],
    "properties": {
      "customerId": {"type": "string", "minLength": 1},
      "items": {
        "type": "array",
        "minItems": 1,
        "items": {
          "type": "object",
          "required": ["sku", "quantity"],
          "properties": {
            "sku": {"type": "string"},
            "quantity": {"type": "integer", "minimum": 1, "maximum": 100}
          }
        }
      },
      "deliveryDate": {"type": "string", "format": "date"}
    }
  }'

요청 검증 실패 시 Lambda 호출 없이 즉시 반환되는 응답:

{
  "message": "Invalid request body"
}

Mock 통합: 개발 초기 단계와 OPTIONS 처리

Mock 통합은 백엔드 없이 정적 응답을 반환한다. 사용 케이스:

  1. 개발 초기: 백엔드가 준비되기 전에 프론트엔드 개발 진행
  2. OPTIONS preflight: REST API + Lambda Proxy에서 CORS OPTIONS 처리
  3. Health check: /health 엔드포인트를 Lambda 없이 처리
# Mock 통합으로 OPTIONS 처리 (CORS preflight)
aws apigateway put-integration \
  --rest-api-id abc123 \
  --resource-id res001 \
  --http-method OPTIONS \
  --type MOCK \
  --request-templates '{"application/json": "{\"statusCode\": 200}"}'
 
aws apigateway put-integration-response \
  --rest-api-id abc123 \
  --resource-id res001 \
  --http-method OPTIONS \
  --status-code 200 \
  --response-parameters '{
    "method.response.header.Access-Control-Allow-Origin": "'"'"'*'"'"'",
    "method.response.header.Access-Control-Allow-Methods": "'"'"'GET,POST,DELETE,OPTIONS'"'"'",
    "method.response.header.Access-Control-Allow-Headers": "'"'"'Content-Type,Authorization'"'"'"
  }'

정리

API Gateway 통합 유형을 선택하는 기준은 단순하다. 대부분의 경우는 Lambda Proxy(AWS_PROXY) — 빠르고 단순하다. Lambda 없이 AWS 서비스를 직접 호출하고 싶을 때는 AWS Service Integration + VTL. 요청·응답 변환이 복잡하거나 Lambda 코드를 순수하게 유지하고 싶을 때는 Lambda non-Proxy + VTL. CORS OPTIONS 처리나 개발 단계 목업은 Mock.

다음 글에서는 API Gateway의 보안 계층 — Lambda Authorizer의 내부 동작, Cognito와의 통합, 캐싱으로 비용 절감, 스로틀링 계층 구조 — 를 다룬다.

📝 연습 문제

선택지를 클릭하면 정답·해설이 펼쳐집니다.

문제 1

Lambda Proxy(AWS_PROXY) 통합에서 Lambda 함수가 반환해야 하는 최소 필수 필드는?

문제 2

Lambda Proxy 통합에서 API Gateway 자체가 401 오류를 반환할 때 CORS 헤더가 없어서 브라우저에서 CORS 에러로 표시된다. 가장 적절한 해결책은?

문제 3

API Gateway에서 Lambda 없이 DynamoDB에 직접 Item을 Put하려면 어떤 통합 유형과 설정이 필요한가?

문제 4

VTL 매핑 템플릿에서 Lambda Authorizer가 반환한 userId 값을 DynamoDB 요청에 포함시키려면 어떤 변수를 사용하는가?

문제 5

REST API에서 요청 검증(Request Validation)을 활성화했을 때의 장점은?

문제 6

Lambda Proxy 통합에서 event['queryStringParameters']를 안전하게 접근하는 방법은?

이전API Gateway REST API: 요청이 백엔드에 도달하기까지의 전체 경로Week 4 · Day 1다음 API Gateway 보안·캐싱·스로틀링: SigV4부터 Token Bucket까지 요청이 통제되는 방법Week 4 · Day 3

참고 공식 자료

  • DVA-C02 공식 시험 안내(새 탭에서 열림)
  • AWS Lambda 공식 문서(새 탭에서 열림)
  • Amazon API Gateway 공식 문서(새 탭에서 열림)
  • Amazon DynamoDB 공식 문서(새 탭에서 열림)
  • AWS IAM 공식 문서(새 탭에서 열림)
  • Amazon Cognito 공식 문서(새 탭에서 열림)

이 페이지

  • 다섯 가지 통합 유형의 결정 트리
  • AWS_PROXY (Lambda Proxy): 기본 통합으로 쓰는 이유
  • AWS (Lambda Non-Proxy): VTL로 요청과 응답을 변환
  • AWS 서비스 직접 통합: Lambda 없이 DynamoDB CRUD
  • Gateway Response: API Gateway 자체 에러의 커스터마이징
  • VTL 핵심 변수와 함수
  • CORS 처리: 통합 방식별 올바른 구현
  • Binary Media Types: 이미지·PDF 처리
  • 요청 검증: Lambda 호출 전에 차단하기
  • Mock 통합: 개발 초기 단계와 OPTIONS 처리
  • 정리
  • 연습 문제