FastAPI vs Express — 2025년 백엔드 프레임워크 선택 기준
프레임워크 전쟁이 아닌 선택의 기준
2025년도 중반쯤, 우리 회사에서 새로운 마이크로서비스를 개발할 때 흥미로운 일이 일어났다. Python 팀은 FastAPI를 강력히 밀었고, Node.js 팀은 Express를 옹호했다. 단순히 "우리 언어가 낫다"는 주장이 아니라, 각각의 기술적 이점을 들고나온 것이었다. 내가 개발하면서 배운 건, 프레임워크 선택은 객관적인 성능 수치보다 "우리 팀의 운영 환경"에 맞는가가 훨씬 중요하다는 것이다.
성능 벤치마크 — 숫자가 말하는 것과 말하지 않는 것
먼저 성능부터 비교해보자. 많은 사람들이 TechEmpower 벤치마크를 본다. FastAPI는 대부분의 시나리오에서 Express보다 높은 처리량(requests/sec)을 보여준다. 특히 I/O 바운드 작업에서는 더욱 그렇다. 평균적으로 FastAPI가 약 20-30% 더 높은 throughput을 달성하는 걸 봤다.
하지만 내가 경험한 바로는, 프로덕션 환경에서 이 10-20%의 성능 차이가 실제로 의미있는 경우는 드물다. 왜냐하면:
- 대부분의 병목은 데이터베이스나 외부 API 호출에서 발생한다
- 제대로 설정된 로드 밸런싱이 있으면 단일 인스턴스의 성능 차이는 무시할 수 있다
- 캐싱 전략이 성능을 좌우한다
내가 봤던 실제 사례: 한 회사에서 Express 서버를 Redis 캐싱으로 최적화했더니, FastAPI로 마이그레이션하지 않아도 충분했다. 반대로 다른 회사는 FastAPI로 시작했지만 운영 팀이 Python을 모르면서 운영 난제에 직면했다.
타입 안전성 — 개발 속도와 버그의 trade-off
이건 프레임워크 자체보다는 언어의 특성이지만, 매우 중요하다.
FastAPI는 Python 3.7+의 타입 힌팅을 기본으로 한다:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
is_active: Optional[bool] = True
@app.post("/users/")
async def create_user(user: User) -> User:
# 입력값 검증과 응답 생성이 자동으로 처리됨
# IDE 자동완성과 타입 체킹이 완벽함
return user
Express에서 같은 일을 하려면 TypeScript를 추가해야 한다:
import express, { Request, Response } from 'express';
interface User {
id: number;
name: string;
email: string;
is_active?: boolean;
}
app.post('/users', async (req: Request, res: Response): Promise<void> => {
const user: User = req.body;
// TypeScript는 타입 체킹만 함, 런타임 검증은 별도 라이브러리 필요
// zod, joi, class-validator 등을 추가로 써야 함
res.json(user);
});
FastAPI의 장점: Pydantic이 자동으로 입력 검증을 해준다. 잘못된 데이터가 들어오면 자동으로 400 에러가 반환된다.
Express의 장점: JavaScript 커뮤니티가 TypeScript에 익숙하고, 수많은 검증 라이브러리가 있다.
실무 경험으로 봐서, 타입 안전성이 버그를 줄여주는 건 확실하다. 하지만 팀의 타입스크립트 숙련도가 낮으면 오히려 개발 속도가 떨어진다.
미들웨어 생태계 — 얼마나 성숙한가?
Express는 2010년부터 시작된 검증된 생태계를 가지고 있다. 거의 모든 니즈를 충족하는 미들웨어가 있다:
- 인증: passport, jsonwebtoken
- 요청 검증: express-validator, joi
- 로깅: morgan, winston
- CORS, 압축, 속도 제한 등 모두 있다
FastAPI는 상대적으로 더 젊지만, 최근 몇 년간 빠르게 성장했다. 기본 기능들이 프레임워크에 내장되어 있어서 추가 미들웨어가 필요 없는 경우가 많다:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZIPMiddleware
from slowapi import Limiter
from slowapi.util import get_remote_address
app = FastAPI()
# CORS는 내장
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 압축도 내장
app.add_middleware(GZIPMiddleware, minimum_size=1000)
# 속도 제한
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
Express의 장점: 문제가 생기면 Stack Overflow에 이미 답변이 있을 확률이 높다.
FastAPI의 장점: 구성이 명확하고, 대부분 한 곳에서 관리할 수 있다.
비동기 지원 — 중요하지만 선택 기준은 아니다
Express는 기본적으로 동기식이지만, async/await를 지원한다.
FastAPI는 비동기를 중심으로 설계되었다. ASGI 서버를 사용한다.
그런데 여기서 중요한 건, 비동기가 항상 빠른 건 아니라는 점이다. CPU 바운드 작업(복잡한 계산, 이미지 처리 등)에서는 비동기가 도움이 안 된다. 내가 봤던 경우들:
- I/O 바운드 작업(DB 쿼리, API 호출): 비동기가 진짜 도움이 된다
- CPU 바운드 작업: 스레드나 프로세스 풀이 필요하다
FastAPI 예제:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
# 여러 I/O 작업을 동시에 처리
user = await db.get_user(user_id)
posts = await db.get_user_posts(user_id)
comments = await db.get_user_comments(user_id)
# asyncio.gather를 써도 되고
# [user, posts, comments] = await asyncio.gather(
# db.get_user(user_id),
# db.get_user_posts(user_id),
# db.get_user_comments(user_id)
# )
return {"user": user, "posts": posts, "comments": comments}
Express에서 같은 일:
app.get('/users/:userId', async (req, res) => {
try {
const [user, posts, comments] = await Promise.all([
db.getUser(req.params.userId),
db.getUserPosts(req.params.userId),
db.getUserComments(req.params.userId)
]);
res.json({ user, posts, comments });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Express도 충분히 비동기를 잘 처리한다. 문법이 조금 다를 뿐이다.
배포 패턴과 운영
내 경험상, 이 부분이 프레임워크 선택을 좌우하는 가장 큰 요소다.
Express는 Docker에서 Node.js 런타임만 필요하다:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
FastAPI는 Python이 필요하고, C 확장이 있는 라이브러리들(numpy, cryptography 등)이 컴파일되어야 한다:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
차이:
- Node.js 이미지가 더 가볍다 (alpine: ~200MB)
- Python 이미지가 더 크다 (~300-400MB)
- 하지만 최신 Python 3.11 slim으로 내려오면서 격차가 줄었다
운영 측면:
- Express: Node.js 개발자가 많고, npm 생태계가 성숙함
- FastAPI: 데이터 과학팀이 있으면 기존 Python 인프라와 통합하기 좋음
결론 — 우리는 둘 다 썼다
결국 우리 회사가 내린 결론:
- 새로운 마이크로서비스는 FastAPI로 시작 (타입 안전성, 개발 속도)
- 기존 Express 서비스는 유지보수 (팀이 익숙함, 안정성)
- 선택 기준: "팀이 잘 알고 있는가"가 첫 번째, 성능은 두 번째
개발하면서 배운 것: "최고의 기술 스택은 팀이 편하게 운영할 수 있는 것이다."