서버리스 vs 컨테이너 2026: 백엔드 팀을 위한 실용적 의사결정 가이드

우리 팀은 작년 한 해 동안 같은 선택지를 세 번 다시 검토했다. 처음엔 AWS Lambda로 백엔드를 시작했다. 8개월 후엔 메인 API를 ECS Fargate로 옮겼다. 지금은 둘을 섞어 쓰는 하이브리드 구성으로 안착했다. 3명짜리 팀이 인프라 관리 부담을 최대한 줄이면서 프로덕션 시스템을 굴리려다 보니, 이 선택이 생각보다 훨씬 중요하다는 걸 뒤늦게 깨달았다.

2026년에도 이 논쟁은 끝나지 않았다. AWS Lambda SnapStart가 Java 워크로드에서 꽤 성숙해졌고, Cloudflare Workers는 V8 isolate 방식으로 cold start 문제를 전혀 다른 각도로 풀고 있다. Graviton3 기반 ECS는 비용 대비 성능에서 예전보다 훨씬 매력적이다. 근데 마케팅 자료만 보면 어느 쪽이든 “내 문제를 완벽하게 해결해준다”고 하니까, 실제로 뭘 고를지가 오히려 더 헷갈린다.

직접 겪은 것들을 정리해봤다.

Lambda가 실제로 잘 작동하는 케이스: 예상보다 범위가 좁다

서버리스를 처음 도입할 때 나는 솔직히 너무 낙관적이었다. “관리할 인프라가 없다”는 말에 혹해서 API Gateway + Lambda 조합으로 백엔드 전체를 구성했는데, 3개월 후에 몇 가지를 다시 뜯어고쳐야 했다.

Lambda가 진짜 빛나는 건 트래픽이 불규칙한 워크로드다. 우리가 운영하는 서비스 중 하나는 주말에 트래픽이 평일의 8~10배 가까이 튀는 구조인데, 여기서는 Lambda가 답이었다. 피크 때 자동으로 수백 개의 인스턴스가 뜨고 평일에는 거의 0에 수렴한다. 같은 걸 ECS로 구성하려면 Auto Scaling 설정을 정교하게 잡아야 하고, 피크를 어느 정도 예측해서 capacity를 미리 확보해야 하는데 — 그 오버헤드가 생각보다 크다.

이벤트 드리븐 파이프라인도 Lambda가 깔끔하다. S3에 파일이 올라오면 처리하고, SQS 메시지 들어오면 처리하는 패턴은 트리거-처리-완료 사이클이 명확하고 상태를 따로 관리할 필요가 없다. 배포 단위도 작아서 개별 함수를 독립적으로 업데이트하기 편하다.

그런데 HTTP API를 Lambda로 전부 처리하려고 하면? 여기서 현실이 달라지기 시작한다.

우리 API 중 Prisma ORM을 쓰는 엔드포인트가 있었다. Prisma는 Node.js 런타임에서 번들 크기가 꽤 크고, 데이터베이스 연결 풀 초기화에 시간이 걸린다. cold start가 발생하면 첫 번째 요청이 1.2~1.8초까지 걸렸다. Lambda SnapStart가 Java에서는 효과적이었지만, Node.js 지원은 2025년 말 기준으로 아직 안정화되지 않았고 내 TypeScript 코드에선 그냥 쓸 수가 없었다.

// Lambda 핸들러 바깥에 DB 클라이언트를 두는 패턴
// warm 상태에서는 연결 재사용, cold start 시엔 새로 초기화됨
let prisma: PrismaClient | null = null;

function getClient(): PrismaClient {
  if (!prisma) {
    prisma = new PrismaClient({
      datasourceUrl: process.env.DATABASE_URL,
      log: process.env.NODE_ENV === 'development' ? ['query'] : [],
    });
  }
  return prisma;
}

export const handler = async (event: APIGatewayProxyEvent) => {
  const db = getClient(); // warm이면 재사용, cold면 1~2초 대기

  try {
    const result = await db.user.findMany({ take: 10 });
    return { statusCode: 200, body: JSON.stringify(result) };
  } catch (error) {
    console.error('Handler error:', error);
    return { statusCode: 500, body: 'Internal error' };
  }
};

이 패턴으로 warm 인스턴스 성능은 괜찮아졌다. 근데 cold start 문제 자체는 여전했다. Provisioned Concurrency를 켜면 해결되긴 하는데 — 그 비용을 계산해보면 다른 이야기가 펼쳐진다.

비용 계산: 청구서를 받고서야 깨달은 것들

서버리스가 항상 싸다는 믿음은 특정 트래픽 패턴에서 완전히 무너진다.

우리 서비스의 메인 API는 하루에 약 400~500만 건의 요청을 처리한다. 처음엔 Lambda로 돌렸다. 첫 달 청구서를 받았을 때 숫자를 두 번 확인했다.

구성월 비용
Lambda 단독 (256MB, 평균 180ms)~$380
Lambda + Provisioned Concurrency 15개~$580
ECS Fargate (t3.medium 2개 + ALB)~$165

트래픽이 어느 선을 넘고 예측 가능한 패턴이면, 컨테이너가 더 싸다. AWS 가격표를 조금만 들여다봐도 계산할 수 있는 건데, 나는 실제 청구서가 날아오기 전까지 제대로 안 했다. 초반 편의성에 안주했다는 게 솔직한 이유다.

반대 케이스도 있다. 월 50만 건 이하인 내부 어드민 API는 Lambda로 두는 게 훨씬 저렴하다. ECS를 항상 켜두는 비용(월 $50~80)이 그 API의 Lambda 실행 비용보다 크기 때문이다.

근데 비용만 보면 절반만 보는 거다. 운영 복잡도와 개발자 시간 비용을 숫자로 환산하면 어떻게 될까? 이게 더 중요한 질문이었는데, 나는 너무 늦게 물어봤다.

프로덕션 장애 대응에서 드러난 진짜 차이

Lambda로 프로덕션을 굴리면서 가장 힘들었던 건 장애 대응이었다.

에러가 났을 때 CloudWatch Logs를 뒤지면, 여러 인스턴스에서 로그가 시간 순서로 뒤섞여서 들어온다. 특정 요청의 흐름을 추적하려면 AWS X-Ray를 제대로 세팅해야 하고, Correlation ID를 모든 로그에 붙이는 구조를 미리 만들어둬야 한다. 그걸 갖추는 데 이틀이 걸렸다. 처음부터 했어야 했는데, 장애가 나고 나서야 했다.

금요일 오후에 배포를 올렸는데 — 이걸 금요일에 한 게 첫 번째 실수였다 — cold start 시간이 갑자기 3초까지 튀는 이슈가 생겼다. CloudWatch Metrics를 보니 init duration이 비정상적으로 늘어나 있었다. 원인 파악에 두 시간이 걸렸다. Lambda 레이어 의존성 중 하나가 최신 버전으로 자동 업데이트되면서, 초기화 코드가 외부 설정 API를 호출하고 있었고 그 API가 타임아웃나고 있었던 거다. 컨테이너였다면 로컬에서 동일한 환경을 재현해서 훨씬 빨리 잡았을 문제다.

컨테이너의 가장 큰 장점은 환경 일관성이다.

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s \
  CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]

docker compose up으로 로컬에서 돌리는 것과 ECS에서 돌리는 게 사실상 동일한 환경이다. Lambda는 런타임이 AWS가 관리하는 영역이라, 로컬 테스트와 실제 실행 환경 사이에 미묘한 차이가 생길 여지가 있다.

응답 시간 분포도 다르다. 컨테이너는 최소 1개 인스턴스가 항상 올라와 있으면 첫 번째 요청이든 천 번째 요청이든 응답 시간이 안정적이다. 우리가 p99 기준 200ms 이하를 SLA로 보장해야 하는 계약이 있었는데, Lambda의 cold start는 그 숫자를 주기적으로 위반했다. ECS로 옮기고 나서 p99가 훨씬 안정적으로 잡혔다.

나는 처음에 이걸 비용 문제라고 생각했는데, 결정적인 요소는 비용이 아니라 팀의 디버깅 패턴과 SLA 요구사항이었다. 이 순서가 바뀌었다는 게 내가 가장 놀란 부분이다.

실제로 쓰는 판단 기준 (2026 기준)

여러 번 결정을 바꿔보면서 나름의 기준이 생겼다. 완벽한 공식은 없고, 예상치 못한 트래픽 스파이크 앞에서는 100% 확신하기 어려운 부분도 있다. 그래도 대략 이런 흐름으로 판단한다.

서버리스를 선택하는 경우:
– 트래픽이 시간대별·요일별로 3배 이상 편차가 있는 경우
– 월 실행량 100만 요청 이하인 저빈도 워크로드
– 이벤트 드리븐, 배치 처리, 웹훅 수신 등 호출 패턴이 명확한 경우
– 팀 규모 2인 이하, 인프라에 신경 쓸 여력이 없는 초기 단계

컨테이너를 선택하는 경우:
– 하루 종일 일정 수준의 트래픽이 유지되는 경우
– p99 응답 시간을 SLA로 보장해야 하는 경우
– ML 모델 서빙, WebSocket, 긴 연결이 필요한 경우
– 로컬-테스트-프로덕션 환경 일관성이 팀 생산성에 직접 영향을 주는 경우

Cloudflare Workers는 2026년 기준으로 진지하게 고려할 만한 세 번째 선택지다. V8 isolate 기반이라 cold start가 거의 없고, 엣지에서 실행되니까 글로벌 응답 시간이 Lambda보다 낫다. 다만 런타임 제약이 있다 — Node.js 전체 API를 쓸 수 없고 CPU 시간 제한도 있다. 우리는 API 게이트웨이 레이어(인증 토큰 검증, 라우팅, 간단한 A/B 플래그 처리)에 써봤는데 그 목적으로는 꽤 좋았다. 복잡한 비즈니스 로직을 거기 넣는 건 아직 내 선택지가 아니다.

EKS나 자체 Kubernetes는? 팀 규모 5명 이하라면 권하지 않는다. ECS Fargate가 K8s의 90%를 훨씬 낮은 운영 복잡도로 제공한다. K8s의 나머지 10% — 세밀한 스케줄링, 커스텀 오퍼레이터, 멀티 클러스터 관리 — 가 실제로 필요한 시점이 오면 그때 올려도 늦지 않다.

지금 내가 선택한 구성과 그 이유

현재 우리 팀의 아키텍처:

  • Lambda: S3 트리거 파일 처리, SQS 컨슈머, 어드민 API (월 30만 요청 이하)
  • ECS Fargate: 메인 REST API, WebSocket 서버
  • Cloudflare Workers: 엣지 인증 검증, API 라우팅

처음부터 이걸 설계한 게 아니다. Lambda로 시작해서 문제가 생기면 하나씩 옮겼다. 역방향 — 컨테이너 먼저 시작해서 Lambda로 일부 옮기는 — 도 나쁘지 않다. 어느 방향이든 실제 트래픽이 들어오기 전까지는 잘못된 가정을 만들 가능성이 높다. 처음부터 완벽한 아키텍처를 잡으려는 시도가 오히려 시간을 더 잡아먹는다.

내 실제 권고는 이렇다. 팀이 3명 이하이고 초기 단계라면 Lambda로 시작해라. 관리 오버헤드를 최소화하면서 프로덕트 자체에 집중할 수 있다. 그리고 트래픽이 안정화되는 3개월 시점에 비용과 p99 응답 시간을 직접 측정해라. 그 숫자가 나오면 컨테이너로 전환할지 판단이 훨씬 쉬워진다.

이벤트 드리븐 처리와 배치 작업은 Lambda에 그냥 놔두는 게 대부분 맞다. cold start가 문제가 되는 경우가 드물고, 자동 스케일링이 자연스럽게 작동한다.

두 도구 다 2026년에 유효하다. 모든 걸 하나로 통일하는 게 최선이라는 생각을 버리는 것부터 시작하면 된다.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top