🔌 REST API 설계

120

왜 REST API를 직접 만들어 볼까?

여러분이 모바일 앱 개발 팀에서 일하고 있다고 상상해 보세요. 기획팀에서 "할 일 관리 앱"을 만들자고 합니다. iOS 앱, Android 앱, 웹 앱 세 가지 플랫폼에서 동일한 데이터를 사용해야 합니다.

REST API 리소스 설계 다이어그램 — CRUD 메서드와 상태 코드

이때 각 플랫폼이 직접 데이터베이스에 접속하면 어떻게 될까요?

  • 데이터베이스 접속 정보가 앱에 포함되어 보안 위험
  • 데이터 검증 로직을 세 곳에서 각각 구현해야 하는 중복 코드
  • 데이터베이스 구조를 변경하면 모든 앱을 동시에 업데이트해야 하는 강한 결합

해결책은 REST API입니다. 모든 플랫폼이 동일한 HTTP 엔드포인트를 통해 데이터에 접근합니다. POST /items로 할 일을 추가하고, GET /items로 목록을 조회하고, DELETE /items/123으로 삭제합니다. API가 중간 계층 역할을 하면서 인증, 검증, 비즈니스 로직을 한곳에서 관리합니다.

이 실습에서는 AWS의 서버리스 서비스로 프로덕션 수준의 REST API를 직접 만듭니다:

  • API Gateway: HTTP 엔드포인트 노출 + 라우팅
  • Lambda: 비즈니스 로직 (CRUD 처리)
  • DynamoDB: 데이터 저장소
  • Cognito: 사용자 인증 (JWT 토큰 기반)

서버 없이, 완전히 서버리스로, 인증까지 갖춘 REST API를 만들어 보겠습니다.

실습을 시작하기 전에 AWS 콘솔에 로그인되어 있는지 확인하세요. 리전은 ap-northeast-2 (서울) 을 사용합니다.

아키텍처 개요

REST API HTTP 메서드 — CRUD 매핑 가이드

API 요청 흐름

다이어그램 로딩 중...
REST API 요청 처리 흐름

비용 예측

비용 계산기

2시간
0h24h
API Gateway

1,000 요청당 $3.50

$0.0080
Lambda

100만 요청당 $0.20 + 실행시간

$0.0040
DynamoDB

쓰기 $1.25/백만, 읽기 $0.25/백만

$0.0060
예상 총 비용$0.0180

* 실제 비용은 AWS 요금 정책에 따라 달라질 수 있습니다.

Step 1: DynamoDB 테이블 생성

할 일(Item) 데이터를 저장할 테이블을 만듭니다. 각 항목은 고유 ID, 제목, 완료 여부, 생성일 등을 포함합니다.

  1. AWS 콘솔 → DynamoDB 검색 → 테이블테이블 생성 클릭
  2. 테이블 이름: lab-items
  3. 파티션 키: id (문자열) — 각 할 일 항목의 고유 식별자
  4. 정렬 키: 비워둠 (단순 키-값 구조)
  5. 테이블 설정 → 설정 사용자 지정 → 용량 모드: 온디맨드 선택
  6. 테이블 생성 클릭 → 상태가 활성이 될 때까지 대기
코드
aws dynamodb create-table \
  --table-name lab-items \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

온디맨드 모드를 선택하면 사전에 읽기/쓰기 용량을 설정할 필요 없이 실제 사용량에 따라 자동으로 스케일링됩니다. 개발 및 테스트 환경에서는 온디맨드가 경제적이고, 트래픽 패턴이 예측 가능한 프로덕션에서는 프로비저닝 모드가 저렴할 수 있습니다.

Step 2: Lambda 함수 작성 (CRUD)

하나의 Lambda 함수에서 HTTP 메서드에 따라 CREATE, READ, UPDATE, DELETE를 모두 처리합니다.

진행률 0/7
  1. 1Lambda 콘솔 → 함수 생성 → 새로 작성
  2. 2함수 이름: lab-items-handler, 런타임: Node.js 20.x 선택
  3. 3실행 역할: 기본 Lambda 권한을 가진 새 역할 생성
  4. 4함수 생성 후 IAM 콘솔에서 역할에 AmazonDynamoDBFullAccess 정책 추가
  5. 5환경 변수 추가: TABLE_NAME = lab-items
  6. 6아래 코드를 코드 편집기에 붙여넣고 Deploy 클릭
  7. 7테스트 탭에서 GET 요청 시뮬레이션으로 동작 확인
코드
// CRUD Lambda 핸들러 (Node.js 20.x)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { PutCommand, GetCommand, ScanCommand, UpdateCommand, DeleteCommand, DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import crypto from 'crypto';
 
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.TABLE_NAME || 'lab-items';
 
export const handler = async (event) => {
  const method = event.httpMethod;
  const id = event.pathParameters?.id;
 
  try {
    switch (method) {
      case 'GET':
        if (id) {
          const { Item } = await client.send(new GetCommand({ TableName: TABLE, Key: { id } }));
          if (!Item) return response(404, { error: '항목을 찾을 수 없습니다' });
          return response(200, Item);
        }
        const { Items } = await client.send(new ScanCommand({ TableName: TABLE }));
        return response(200, Items);
 
      case 'POST': {
        const body = JSON.parse(event.body);
        const item = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString(), completed: false };
        await client.send(new PutCommand({ TableName: TABLE, Item: item }));
        return response(201, item);
      }
      case 'PUT': {
        const body = JSON.parse(event.body);
        await client.send(new UpdateCommand({
          TableName: TABLE, Key: { id },
          UpdateExpression: 'SET title = :t, completed = :c, updatedAt = :u',
          ExpressionAttributeValues: { ':t': body.title, ':c': body.completed, ':u': new Date().toISOString() },
        }));
        return response(200, { message: '수정 완료' });
      }
      case 'DELETE':
        await client.send(new DeleteCommand({ TableName: TABLE, Key: { id } }));
        return response(200, { message: '삭제 완료' });
 
      default:
        return response(405, { error: '지원하지 않는 메서드입니다' });
    }
  } catch (err) {
    return response(500, { error: err.message });
  }
};
 
const response = (statusCode, body) => ({
  statusCode,
  headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' },
  body: JSON.stringify(body),
});

Step 3: API Gateway REST API 생성

Lambda 함수를 HTTP 엔드포인트로 노출합니다. RESTful URL 구조를 설계하고 각 메서드에 Lambda를 연결합니다.

  1. API Gateway 콘솔 → REST API빌드 클릭
  2. 새 API 선택, API 이름: lab-items-apiAPI 생성 클릭
  3. 리소스 생성: 이름 items, 경로 /items리소스 생성 클릭
  4. /items 선택 → 메서드 생성GET → Lambda 프록시 통합 → lab-items-handler 선택
  5. 같은 방식으로 /itemsPOST 메서드 추가
  6. /items 선택 → 리소스 생성 → 경로에 {id} 입력 → 리소스 생성 클릭
  7. /items/{id} 선택 → GET, PUT, DELETE 메서드 각각 추가 → 모두 lab-items-handler Lambda 연결
  8. 각 리소스에서 CORS 활성화 클릭 (브라우저에서 테스트하려면 필수)
  9. API 배포 → 스테이지 이름: dev → 호출 URL 복사
코드
# REST API 생성
aws apigateway create-rest-api \
  --name lab-items-api \
  --endpoint-configuration types=REGIONAL
 
# 리소스 및 메서드는 콘솔에서 구성을 권장합니다

Step 4: Cognito 인증 연동

API를 외부에 노출하면 누구나 데이터를 생성/삭제할 수 있습니다. Cognito User Pool을 만들어 JWT 토큰 기반 인증을 적용합니다.

진행률 0/12
  1. 1AWS 콘솔에서 Cognito 검색 → 사용자 풀 생성 클릭
  2. 2인증 공급자: Cognito 사용자 풀 선택
  3. 3로그인 옵션: 이메일 체크
  4. 4암호 정책: 기본값 유지 (8자 이상, 대소문자+숫자+특수문자)
  5. 5MFA: MFA 없음 선택 (실습 편의상)
  6. 6사용자 풀 이름: lab-api-users
  7. 7앱 클라이언트 이름: lab-api-client → 클라이언트 시크릿 생성하지 않음 체크
  8. 8사용자 풀 생성 클릭
  9. 9API Gateway 콘솔로 이동 → lab-items-api → 권한 부여자 탭
  10. 10권한 부여자 생성 → 유형: Cognito, 사용자 풀: lab-api-users 선택, 토큰 소스: Authorization
  11. 11각 메서드 → 메서드 요청 → 권한 부여: 생성한 Cognito 권한 부여자 선택
  12. 12API 재배포 (dev 스테이지) → 토큰 없이 요청하여 401 Unauthorized 확인

Cognito 권한 부여자를 설정한 후 반드시 API를 재배포하세요. 배포하지 않으면 이전 설정(인증 없음)이 그대로 적용됩니다.

Step 5: API 배포 후 인증 없이 테스트

Cognito 인증을 적용하기 전에 먼저 API가 정상적으로 동작하는지 확인합니다.

진행률 0/8
  1. 1API Gateway에서 dev 스테이지로 배포한 호출 URL을 복사합니다
  2. 2POST /items 테스트 — 할 일 생성: curl -X POST https://YOUR_API_URL/dev/items \ -H "Content-Type: application/json" \ -d '{"title": "AWS 실습 완료하기"}'
  3. 3응답에서 id 값을 확인합니다 (예: "id": "a1b2c3d4-...")
  4. 4GET /items 테스트 — 목록 조회: curl https://YOUR_API_URL/dev/items
  5. 5GET /items/ 테스트 — 단건 조회: curl https://YOUR_API_URL/dev/items/{위에서_받은_id}
  6. 6PUT /items/ 테스트 — 수정: curl -X PUT https://YOUR_API_URL/dev/items/{id} \ -H "Content-Type: application/json" \ -d '{"title": "AWS 실습 완료!", "completed": true}'
  7. 7DELETE /items/ 테스트 — 삭제: curl -X DELETE https://YOUR_API_URL/dev/items/{id}
  8. 8삭제 후 GET으로 404 응답 확인

모든 CRUD가 정상 동작하는 것을 확인한 후에 Cognito 인증을 적용하세요. 인증과 비즈니스 로직을 동시에 디버깅하면 문제 원인을 찾기 어렵습니다. 단계적으로 기능을 추가하는 것이 서버리스 개발의 좋은 습관입니다.

Step 6: 인증이 적용된 API 테스트

Cognito 인증을 적용한 후 JWT 토큰을 포함한 요청을 테스트합니다.

진행률 0/8
  1. 1Cognito 콘솔 → lab-api-users → 사용자 탭 → 사용자 생성 → 이메일 + 임시 비밀번호 설정
  2. 2AWS CLI로 토큰 발급: aws cognito-idp initiate-auth \ --client-id YOUR_CLIENT_ID \ --auth-flow USER_PASSWORD_AUTH \ --auth-parameters USERNAME=test@example.com,PASSWORD=TempPass123!
  3. 3반환된 IdToken을 복사합니다
  4. 4POST 테스트 (항목 생성): curl -X POST https://YOUR_API_URL/dev/items \ -H "Authorization: YOUR_ID_TOKEN" \ -H "Content-Type: application/json" \ -d '{"title": "AWS 실습 완료하기"}'
  5. 5GET 테스트 (목록 조회): curl -H "Authorization: TOKEN" https://YOUR_API_URL/dev/items
  6. 6GET 테스트 (단건 조회): curl -H "Authorization: TOKEN" https://YOUR_API_URL/dev/items/{id}
  7. 7PUT 테스트 (수정): title과 completed 값을 변경 후 재조회
  8. 8DELETE 테스트 (삭제): 삭제 후 GET으로 404 확인

트러블슈팅

"Unauthorized" (401) 에러 — 토큰을 보냈는데도 실패하면:

  1. 토큰이 만료되지 않았는지 확인 (기본 1시간)
  2. Authorization 헤더 이름이 정확한지 확인 (대소문자 구분)
  3. IdToken을 사용하세요 (AccessToken이 아님)

CORS 에러가 나오면: 브라우저에서 테스트할 때 OPTIONS 프리플라이트 요청이 실패하는 경우입니다. 각 리소스에서 CORS를 활성화하고, Lambda 응답에 Access-Control-Allow-Origin: * 헤더를 포함했는지 확인하세요.

"Internal Server Error" — Lambda 로그에 "Cannot read property 'id' of undefined": event.pathParameters가 null인 경우입니다. /items(목록 조회)와 /items/{id}(단건 조회)에서 pathParameters 존재 여부를 먼저 체크하는 방어 코드가 필요합니다 (위 코드의 ?. 연산자 참고).

완성 후 통합 테스트 체크리스트

Cognito 인증이 적용된 상태에서 모든 기능을 최종 점검합니다.

진행률 0/7
  1. 1인증 없이 요청 → 401 Unauthorized 반환 확인
  2. 2잘못된 토큰으로 요청 → 401 Unauthorized 반환 확인
  3. 3유효한 토큰으로 POST → 항목 생성 성공 (201)
  4. 4GET /items → 생성한 항목이 목록에 포함되어 있는지 확인
  5. 5PUT /items/ → 수정 후 GET으로 변경 내용 확인
  6. 6DELETE /items/ → 삭제 후 GET으로 404 확인
  7. 7DynamoDB 콘솔 → lab-items 테이블에서 데이터가 API 응답과 일치하는지 확인

핵심 개념 확인

확장 아이디어

기본 CRUD API가 완성되었으니, 더 발전시켜 보세요:

  1. 페이지네이션 구현: DynamoDB의 LimitExclusiveStartKey를 활용한 커서 기반 페이지네이션
  2. 검색 필터: Query String으로 ?completed=true 같은 필터링 기능 추가 (FilterExpression 활용)
  3. 사용자별 데이터 격리: Cognito의 sub(사용자 ID)을 활용하여 본인 데이터만 접근 가능하도록
  4. API 사용량 제한: API Gateway의 Usage Plan으로 API Key 발급 및 요청 제한 (Rate Limiting)
  5. OpenAPI 문서 자동 생성: API Gateway에서 Swagger/OpenAPI 스펙 내보내기

학습 정리

핵심 치트시트

리소스 정리

실습 완료 후 반드시 아래 순서대로 리소스를 정리하여 불필요한 과금을 방지하세요.

  1. API Gateway → API 삭제
  2. Lambda 함수 삭제 (lab-items-handler)
  3. DynamoDB 테이블 삭제 (lab-items)
  4. Cognito 사용자 풀 삭제
  5. IAM 역할 정리 (Lambda 실행 역할)
  6. CloudWatch 로그 그룹 삭제