Serverless 아키텍처 실전 가이드 - AWS Lambda부터 Vercel까지

Serverless 아키텍처 실전 가이드

Serverless 아키텍처는 서버 관리 없이 코드만 배포하면 자동으로 실행되는 클라우드 컴퓨팅 모델이다. 인프라 관리 부담을 줄이고, 비용을 최적화하며, 자동 스케일링을 제공한다. 이 글은 Serverless의 개념부터 실전 배포까지 완벽하게 정리한다.

1. Serverless 아키텍처란?

1-1. 기본 개념

Serverless는 “서버가 없다”는 의미가 아니라, “서버를 관리할 필요가 없다”는 의미다. 개발자는 코드만 작성하고 배포하면, 클라우드 제공자가 서버 관리, 스케일링, 모니터링을 자동으로 처리한다.

전통적인 서버 아키텍처:

개발자 → 서버 설정 → 애플리케이션 배포 → 모니터링 → 스케일링 관리

Serverless 아키텍처:

개발자 → 코드 작성 → 배포 → 자동 실행
         (서버 관리, 스케일링 자동)

1-2. Serverless의 특징

장점:

  • 서버 관리 불필요: 인프라 관리 부담 제거
  • 자동 스케일링: 트래픽에 따라 자동 확장/축소
  • 비용 효율: 사용한 만큼만 비용 지불
  • 빠른 배포: 코드만 배포하면 즉시 실행
  • 고가용성: 자동으로 여러 리전에 배포

단점:

  • Cold Start: 첫 실행 시 지연 시간 발생
  • 실행 시간 제한: 플랫폼별 제한 시간 존재
  • 벤더 종속: 특정 클라우드 제공자에 종속
  • 디버깅 어려움: 로컬 환경과 차이
  • 상태 관리 제한: 상태 저장 어려움

1-3. Serverless 사용 사례

적합한 경우:

  • API 엔드포인트
  • 이벤트 기반 처리 (파일 업로드, 메시지 큐)
  • 크론 작업 (정기 실행 작업)
  • 마이크로서비스
  • 정적 사이트 + 동적 기능

부적합한 경우:

  • 장시간 실행 작업
  • 실시간 스트리밍
  • 상태 저장이 중요한 애플리케이션
  • 매우 낮은 지연 시간 요구

2. 주요 Serverless 플랫폼 비교

2-1. 플랫폼 개요

플랫폼 제공사 주요 특징 가격 모델
AWS Lambda Amazon 가장 성숙, 다양한 통합 요청 수 + 실행 시간
Vercel Functions Vercel 프론트엔드 통합 우수 무료 플랜 + 사용량
Netlify Functions Netlify 정적 사이트 통합 무료 플랜 + 사용량
Google Cloud Functions Google GCP 생태계 통합 요청 수 + 실행 시간
Azure Functions Microsoft 엔터프라이즈 기능 요청 수 + 실행 시간

2-2. 상세 비교

AWS Lambda

  • 언어 지원: Node.js, Python, Java, Go, Ruby, .NET
  • 최대 실행 시간: 15분
  • 메모리: 128MB ~ 10GB
  • 트리거: API Gateway, S3, DynamoDB, SNS 등
  • 장점: 가장 성숙, 다양한 서비스 통합
  • 단점: 설정 복잡, 학습 곡선 높음

Vercel Functions

  • 언어 지원: Node.js, Python, Go
  • 최대 실행 시간: 10초 (Hobby), 60초 (Pro)
  • 메모리: 1GB
  • 트리거: HTTP 요청, Cron
  • 장점: Next.js 통합 우수, 간단한 배포
  • 단점: 실행 시간 제한, 엔터프라이즈 기능 제한

Netlify Functions

  • 언어 지원: Node.js, Go, Python, Ruby
  • 최대 실행 시간: 10초 (Free), 26초 (Pro)
  • 메모리: 1.5GB
  • 트리거: HTTP 요청, Scheduled Functions
  • 장점: 정적 사이트 통합, 간단한 설정
  • 단점: 실행 시간 제한, 확장성 제한

3. AWS Lambda 실전 가이드

3-1. 첫 번째 Lambda 함수 만들기

index.js:

exports.handler = async (event) => {
  const name = event.queryStringParameters?.name || 'World';
  
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      message: `Hello, ${name}!`,
      timestamp: new Date().toISOString(),
    }),
  };
};

배포 (AWS CLI):

# 함수 패키징
zip function.zip index.js

# 함수 생성
aws lambda create-function \
  --function-name hello-world \
  --runtime nodejs20.x \
  --role arn:aws:iam::ACCOUNT:role/lambda-role \
  --handler index.handler \
  --zip-file fileb://function.zip

3-2. API Gateway와 통합

Lambda 함수:

exports.handler = async (event) => {
  const method = event.httpMethod;
  const path = event.path;
  const body = event.body ? JSON.parse(event.body) : {};
  
  if (method === 'GET' && path === '/users') {
    // 사용자 목록 조회
    return {
      statusCode: 200,
      body: JSON.stringify([
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' },
      ]),
    };
  }
  
  if (method === 'POST' && path === '/users') {
    // 사용자 생성
    return {
      statusCode: 201,
      body: JSON.stringify({
        id: Date.now(),
        ...body,
      }),
    };
  }
  
  return {
    statusCode: 404,
    body: JSON.stringify({ error: 'Not Found' }),
  };
};

API Gateway 설정:

  • REST API 생성
  • 리소스 및 메서드 설정
  • Lambda 함수 연결
  • 배포

3-3. 환경 변수 사용

Lambda 함수:

exports.handler = async (event) => {
  const apiKey = process.env.API_KEY;
  const dbUrl = process.env.DATABASE_URL;
  
  // 환경 변수 사용
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Environment variables loaded',
    }),
  };
};

환경 변수 설정:

aws lambda update-function-configuration \
  --function-name my-function \
  --environment Variables="{API_KEY=secret123,DATABASE_URL=postgres://...}"

3-4. 데이터베이스 연결

DynamoDB 연결 예시:

const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();

exports.handler = async (event) => {
  const userId = event.pathParameters.id;
  
  // DynamoDB에서 데이터 조회
  const result = await dynamodb.get({
    TableName: 'Users',
    Key: { id: userId },
  }).promise();
  
  return {
    statusCode: 200,
    body: JSON.stringify(result.Item),
  };
};

4. Vercel Functions 실전 가이드

4-1. Next.js API Routes

pages/api/hello.js:

export default function handler(req, res) {
  const { name } = req.query;
  
  res.status(200).json({
    message: `Hello, ${name || 'World'}!`,
    timestamp: new Date().toISOString(),
  });
}

App Router (app/api/hello/route.ts):

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const name = searchParams.get('name') || 'World';
  
  return Response.json({
    message: `Hello, ${name}!`,
    timestamp: new Date().toISOString(),
  });
}

export async function POST(request: Request) {
  const body = await request.json();
  
  return Response.json({
    message: 'Data received',
    data: body,
  });
}

4-2. Serverless Functions (api 폴더)

api/users.ts:

import type { VercelRequest, VercelResponse } from '@vercel/node';

export default async function handler(
  req: VercelRequest,
  res: VercelResponse
) {
  if (req.method === 'GET') {
    const users = [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ];
    
    res.status(200).json(users);
  } else if (req.method === 'POST') {
    const { name, email } = req.body;
    
    res.status(201).json({
      id: Date.now(),
      name,
      email,
    });
  } else {
    res.status(405).json({ error: 'Method not allowed' });
  }
}

4-3. 환경 변수 설정

vercel.json:

{
  "env": {
    "DATABASE_URL": "@database-url",
    "API_KEY": "@api-key"
  }
}

Vercel 대시보드에서 설정:

  • Settings → Environment Variables
  • 환경 변수 추가
  • 프로덕션, 프리뷰, 개발 환경별 설정

4-4. Cron Jobs (Scheduled Functions)

api/cron.ts:

import type { VercelRequest, VercelResponse } from '@vercel/node';

export default async function handler(
  req: VercelRequest,
  res: VercelResponse
) {
  // Cron 작업 실행
  console.log('Cron job executed at:', new Date().toISOString());
  
  // 정기 작업 수행
  // 예: 데이터 백업, 리포트 생성 등
  
  res.status(200).json({ message: 'Cron job completed' });
}

vercel.json:

{
  "crons": [
    {
      "path": "/api/cron",
      "schedule": "0 0 * * *"
    }
  ]
}

5. Netlify Functions 실전 가이드

5-1. 함수 생성

netlify/functions/hello.js:

exports.handler = async (event, context) => {
  const { name } = event.queryStringParameters || {};
  
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      message: `Hello, ${name || 'World'}!`,
    }),
  };
};

5-2. TypeScript 함수

netlify/functions/users.ts:

import { Handler } from '@netlify/functions';

const handler: Handler = async (event, context) => {
  if (event.httpMethod === 'GET') {
    return {
      statusCode: 200,
      body: JSON.stringify([
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' },
      ]),
    };
  }
  
  return {
    statusCode: 405,
    body: JSON.stringify({ error: 'Method not allowed' }),
  };
};

export { handler };

5-3. 환경 변수

netlify.toml:

[build]
  functions = "netlify/functions"

[context.production.environment]
  DATABASE_URL = "postgres://..."
  API_KEY = "secret123"

6. Cold Start 문제 해결

6-1. Cold Start란?

Cold Start는 함수가 오랫동안 사용되지 않아 “차가워진” 상태에서 첫 실행 시 발생하는 지연 시간이다.

Cold Start 발생 시나리오:

  1. 함수가 일정 시간 비활성 상태
  2. 첫 요청 시 컨테이너 초기화
  3. 코드 로드 및 실행 환경 준비
  4. 실제 함수 실행

지연 시간:

  • Node.js: 100-500ms
  • Python: 200-1000ms
  • Java: 1-3초
  • .NET: 500ms-2초

6-2. Cold Start 최적화 방법

1. 패키지 크기 최소화

// 나쁜 예: 전체 라이브러리 임포트
const AWS = require('aws-sdk');

// 좋은 예: 필요한 부분만 임포트
const DynamoDB = require('aws-sdk/clients/dynamodb');

2. Provisioned Concurrency (AWS Lambda)

aws lambda put-provisioned-concurrency-config \
  --function-name my-function \
  --qualifier $LATEST \
  --provisioned-concurrent-executions 10

3. 함수 최적화

// 전역 변수 활용 (재사용)
const dbConnection = initializeDatabase();

exports.handler = async (event) => {
  // dbConnection 재사용 (매번 초기화하지 않음)
  const result = await dbConnection.query('SELECT * FROM users');
  return result;
};

4. Keep Warm 패턴

// 주기적으로 함수 호출하여 Warm 상태 유지
exports.handler = async (event) => {
  // CloudWatch Events로 주기적 호출
  if (event.source === 'aws.events') {
    return { statusCode: 200, body: 'Warmed up' };
  }
  
  // 실제 로직
  return { statusCode: 200, body: 'Hello' };
};

7. 비용 최적화

7-1. 비용 구조 이해

AWS Lambda:

  • 요청 수: $0.20 per 1M requests
  • 실행 시간: GB-second당 $0.0000166667
  • 예: 100만 요청, 평균 200ms 실행, 512MB 메모리

    • 요청 비용: $0.20
    • 실행 비용: $0.33
    • 총: $0.53

Vercel:

  • Hobby: 무료 (제한적)
  • Pro: $20/월 + 사용량
  • Enterprise: 맞춤 가격

Netlify:

  • Free: 무료 (제한적)
  • Pro: $19/월 + 사용량
  • Business: $99/월 + 사용량

7-2. 비용 절감 전략

1. 함수 실행 시간 최적화

// 나쁜 예: 불필요한 대기
exports.handler = async (event) => {
  await new Promise(resolve => setTimeout(resolve, 1000)); // 불필요
  return { statusCode: 200 };
};

// 좋은 예: 필요한 작업만 수행
exports.handler = async (event) => {
  // 즉시 처리
  return { statusCode: 200 };
};

2. 메모리 최적화

  • 필요한 만큼만 메모리 할당
  • 메모리가 많을수록 비용 증가

3. 캐싱 활용

const cache = new Map();

exports.handler = async (event) => {
  const key = event.queryStringParameters?.key;
  
  if (cache.has(key)) {
    return {
      statusCode: 200,
      body: JSON.stringify(cache.get(key)),
    };
  }
  
  // 데이터 조회 및 캐싱
  const data = await fetchData(key);
  cache.set(key, data);
  
  return {
    statusCode: 200,
    body: JSON.stringify(data),
  };
};

4. 배치 처리

  • 여러 요청을 하나의 함수로 처리
  • API 호출 최소화

8. 모니터링 및 로깅

8-1. AWS CloudWatch

로깅:

exports.handler = async (event) => {
  console.log('Event received:', JSON.stringify(event));
  console.error('Error occurred:', error);
  
  // 구조화된 로깅
  console.log(JSON.stringify({
    level: 'info',
    message: 'Function executed',
    timestamp: new Date().toISOString(),
    requestId: context.requestId,
  }));
  
  return { statusCode: 200 };
};

메트릭:

  • 실행 횟수
  • 실행 시간
  • 에러율
  • 동시 실행 수

8-2. Vercel Analytics

설정:

// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  );
}

8-3. 에러 처리

에러 핸들링:

exports.handler = async (event) => {
  try {
    // 로직 실행
    const result = await processData(event);
    
    return {
      statusCode: 200,
      body: JSON.stringify(result),
    };
  } catch (error) {
    console.error('Error:', error);
    
    return {
      statusCode: 500,
      body: JSON.stringify({
        error: 'Internal server error',
        message: error.message,
      }),
    };
  }
};

9. 실전 배포 전략

9-1. CI/CD 파이프라인

GitHub Actions (AWS Lambda):

name: Deploy Lambda

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: npm install
      
      - name: Package function
        run: zip -r function.zip index.js node_modules
      
      - name: Deploy to Lambda
        run: |
          aws lambda update-function-code \
            --function-name my-function \
            --zip-file fileb://function.zip

Vercel 자동 배포:

  • GitHub 연동 시 자동 배포
  • 브랜치별 프리뷰 배포
  • 프로덕션 자동 배포

9-2. 환경별 배포

개발/스테이징/프로덕션 분리:

const env = process.env.NODE_ENV || 'development';

const config = {
  development: {
    apiUrl: 'http://localhost:3000',
    databaseUrl: process.env.DEV_DATABASE_URL,
  },
  production: {
    apiUrl: 'https://api.example.com',
    databaseUrl: process.env.PROD_DATABASE_URL,
  },
};

exports.handler = async (event) => {
  const currentConfig = config[env];
  // 설정 사용
};

10. Best Practices

10-1. 함수 설계 원칙

1. 단일 책임 원칙

  • 하나의 함수는 하나의 작업만 수행
  • 작고 집중된 함수 작성

2. Stateless 설계

  • 상태 저장 지양
  • 외부 저장소 활용 (DB, Cache)

3. 에러 처리

  • 모든 에러 처리
  • 적절한 HTTP 상태 코드 반환

4. 타임아웃 설정

  • 적절한 타임아웃 설정
  • 장시간 실행 작업 분리

10-2. 보안

환경 변수 사용:

  • 민감한 정보는 환경 변수로 관리
  • 코드에 하드코딩 금지

권한 최소화:

  • 필요한 권한만 부여
  • IAM 역할 최소 권한 원칙

입력 검증:

exports.handler = async (event) => {
  const { email } = JSON.parse(event.body);
  
  // 입력 검증
  if (!email || !isValidEmail(email)) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error: 'Invalid email' }),
    };
  }
  
  // 처리
};

FAQ

Q: Serverless가 항상 비용 효율적인가요?
A: 트래픽이 불규칙하거나 낮은 경우 비용 효율적이다. 하지만 높은 트래픽이 지속되면 전통적인 서버가 더 경제적일 수 있다.

Q: Cold Start를 완전히 제거할 수 있나요?
A: 완전히 제거는 어렵지만, Provisioned Concurrency나 Keep Warm 패턴으로 최소화할 수 있다.

Q: Serverless로 모든 애플리케이션을 만들 수 있나요?
A: 아니요. 장시간 실행 작업이나 실시간 스트리밍에는 부적합하다. 적절한 사용 사례를 선택하는 것이 중요하다.

Q: 로컬에서 Serverless 함수를 테스트할 수 있나요?
A: 네, AWS SAM, Serverless Framework, Vercel CLI 등으로 로컬 테스트가 가능하다.

Q: 데이터베이스 연결을 어떻게 관리하나요?
A: 연결 풀링을 사용하거나, Serverless에 최적화된 데이터베이스(Aurora Serverless, DynamoDB)를 사용한다.

Q: Serverless 함수의 최대 실행 시간은?
A: 플랫폼마다 다르다. AWS Lambda는 15분, Vercel은 10-60초, Netlify는 10-26초다.

결론: Serverless의 미래

Serverless 아키텍처는 현대 웹 개발의 중요한 패러다임이다. 인프라 관리 부담을 줄이고, 개발자 생산성을 높이며, 비용을 최적화한다.

하지만 모든 문제의 해결책은 아니다. 프로젝트의 요구사항을 정확히 파악하고, Serverless의 장단점을 이해한 후 선택하는 것이 중요하다. 적절히 활용하면 개발 속도를 높이고 운영 부담을 줄일 수 있다.

Serverless는 계속 발전하고 있다. Edge Computing, 더 긴 실행 시간, 더 나은 디버깅 도구 등이 추가되면서 점점 더 실용적인 선택이 되고 있다.

Serverless 학습 후 프로젝트 기회

Serverless 아키텍처를 마스터한 후, 실제 프로젝트에 적용하고 싶다면 블루버튼 같은 프로젝트 매칭 플랫폼을 활용할 수 있다. Serverless를 활용한 API 개발, 마이크로서비스 구축, 정적 사이트 + 동적 기능 구현 등 다양한 프로젝트에서 실전 경험을 쌓을 수 있다. 특히 빠른 프로토타이핑이나 비용 최적화가 중요한 프로젝트에서 Serverless의 강점을 발휘할 수 있다.


Written by Jeon Byung Hun 개발을 즐기는 bottlehs - Engineer, MS, AI, FE, BE, OS, IOT, Blockchain, 설계, 테스트