개발 팁

FastAPI 프로덕션 배포 실전 가이드 — Gunicorn, Docker, 보안 미들웨어까지

노동1호 2026. 4. 16. 06:01

FastAPI로 개발한 API를 로컬에서는 잘 돌아가는데, 프로덕션 환경에 배포하려니 뭘 먼저 해야 할지 막막한 경험, 있으신가요? uvicorn main:app --reload 명령어 하나로 개발할 때는 문제없지만, 실서비스 환경에서는 동시성 처리, 보안, 모니터링, 장애 복구 등 고려할 게 한두 가지가 아닙니다.

FastAPI production deployment architecture diagram

이 글에서는 FastAPI 애플리케이션을 프로덕션에 배포할 때 반드시 알아야 할 핵심 패턴을 단계별로 정리합니다. Docker 컨테이너화부터 Gunicorn 워커 설정, 보안 미들웨어, 헬스체크, 그리고 모니터링까지, 실무에서 바로 적용할 수 있는 코드와 설정을 중심으로 설명하겠습니다.

1. 프로덕션 프로젝트 구조 잡기

프로덕션 환경에서는 기능별로 모듈을 분리하는 것이 필수입니다. 라우터(Router), 서비스(Service), 모델(Model)을 명확히 나누면 유지보수와 확장이 훨씬 쉬워집니다.

my-api/
├── app/
│   ├── main.py              # FastAPI 앱 진입점
│   ├── config/
│   │   └── settings.py      # Pydantic Settings (환경변수 관리)
│   ├── routers/
│   │   ├── auth.py          # 인증 엔드포인트
│   │   └── items.py         # 비즈니스 엔드포인트
│   ├── services/
│   │   └── item_service.py  # 비즈니스 로직
│   ├── models/
│   │   └── schemas.py       # Pydantic 모델
│   └── db/
│       └── database.py      # DB 커넥션 관리
├── Dockerfile
├── docker-compose.yaml
├── gunicorn.conf.py         # Gunicorn 설정
├── pyproject.toml
└── .env.example

이 구조의 핵심은 라우터는 HTTP 처리만, 서비스는 비즈니스 로직만 담당한다는 점입니다. 이렇게 분리하면 새로운 기능을 추가할 때 기존 코드를 건드리지 않아도 됩니다.

2. Pydantic Settings로 환경변수 관리

하드코딩된 설정값은 프로덕션에서 보안 문제를 일으킵니다. Pydantic Settings를 사용하면 타입 안전하게 환경변수를 관리할 수 있습니다.

# app/config/settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Optional

class Settings(BaseSettings):
    PROJECT_NAME: str = "MyAPI"
    API_V1_STR: str = "/api/v1"
    
    # 보안
    SECRET_KEY: str
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    
    # 데이터베이스
    DATABASE_URL: str  # postgresql+asyncpg://...
    
    # Redis (캐시, Celery)
    REDIS_URL: str = "redis://localhost:6379/0"
    
    # CORS
    BACKEND_CORS_ORIGINS: list[str] = ["http://localhost:3000"]
    
    model_config = SettingsConfigDict(
        env_file=".env",
        case_sensitive=True,
        extra="ignore"  # 알 수 없는 환경변수는 무시
    )

settings = Settings()  # 앱 시작 시 필수 변수 누락이면 즉시 에러

Settings()를 호출하는 순간, 필수 필드(SECRET_KEY, DATABASE_URL 등)가 누락되면 앱이 시작되지 않습니다. 이 "Fail Fast" 방식이야말로 프로덕션에서 설정 오류를 조기에 발견하는 최선의 방법입니다.

3. ASGI 서버: Gunicorn + Uvicorn 조합

단일 Uvicorn 프로세스는 개발용입니다. 프로덕션에서는 Gunicorn이 여러 Uvicorn 워커 프로세스를 관리하는 구조를 사용합니다.

# gunicorn.conf.py
import multiprocessing

bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count()  # CPU 코어 수만큼
worker_class = "uvicorn.workers.UvicornWorker"
preload_app = True  # 포크 전 코드 로드 → 메모리 절약

# 워커 재시작 (메모리 누수 방지)
max_requests = 1000
max_requests_jitter = 50  # 1000~1050 요청 후 재시작

# 타임아웃
graceful_timeout = 30
timeout = 120

# 동시 연결
worker_connections = 1000

# 로그에 응답 시간 포함
accesslog = "-"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(D)s'

핵심 포인트:

  • workers = cpu_count(): 동기 워커의 (2 × CPU) + 1 공식과 달리, 비동기 Uvicorn 워커는 코어 수와 동일하게 설정합니다. 하나의 워커가 이미 수천 개의 동시 요청을 처리할 수 있기 때문입니다.
  • max_requests: 장시간 실행 시 메모리 누수가 누적되는 것을 방지합니다. jitter를 주면 모든 워커가 동시에 재시작되는 현상을 막을 수 있습니다.
  • preload_app = True: Copy-on-Write 최적화로 메모리 사용량을 크게 줄입니다.

4. 비동기 데이터베이스 세션 관리

FastAPI는 비동기 프레임워크이므로, 데이터베이스 세션도 비동기로 관리해야 성능이最大化됩니다.

# app/db/database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from app.config.settings import settings

async_engine = create_async_engine(
    settings.DATABASE_URL,
    echo=False,
    pool_size=10,        # 기본 연결 풀 크기
    max_overflow=20,     # 초과 연결 수
    pool_pre_ping=True,  # 연결 유효성 사전 확인
)

AsyncSessionLocal = async_sessionmaker(
    async_engine,
    class_=AsyncSession,
    expire_on_commit=False,
)

# 의존성 주입
async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()
FastAPI production deployment architecture diagram

pool_pre_ping=True는 MySQL의 wait_timeout이나 네트워크 단절로 끊어진 연결을 자동으로 감지하고 복구합니다. pool_size=10, max_overflow=20이면 최대 30개의 동시 DB 연결을 사용할 수 있습니다. 이 값은 앱의 동시 요청 수와 DB 서버의 max_connections 설정에 맞춰 조정해야 합니다.

5. 보안 미들웨어 설정

프로덕션 API에는 CORS, Rate Limiting, 보안 헤더가 필수입니다.

# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from app.config.settings import settings

limiter = Limiter(key_func=get_remote_address)

app = FastAPI(title=settings.PROJECT_NAME)

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.BACKEND_CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Rate Limiting
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# 보안 헤더 미들웨어
@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
    return response

slowapi 라이브러리는 Redis를 백엔드로 사용하는 Rate Limiting을 제공합니다. 엔드포인트별로 @limiter.limit("10/minute") 데코레이터를 추가하면, 분당 10회 요청으로 제한할 수 있습니다.

6. 헬스체크 엔드포인트

로드 밸런서와 오케스트레이션 플랫폼(Kubernetes, ECS 등)은 앱이 정상인지 확인하기 위해 헬스체크를 사용합니다. Liveness(프로세스 살아있음)와 Readiness(의존성 준비됨)를 분리하는 것이 좋습니다.

# app/routers/health.py
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from app.db.database import get_db
import redis

router = APIRouter()

@router.get("/health/live")
async def liveness():
    """프로세스가 살아있는지 확인"""
    return {"status": "alive"}

@router.get("/health/ready")
async def readiness(db: AsyncSession = Depends(get_db)):
    """DB, Redis 등 의존성이 준비되었는지 확인"""
    checks = {}
    
    # DB 연결 확인
    try:
        await db.execute(text("SELECT 1"))
        checks["database"] = "ok"
    except Exception as e:
        checks["database"] = f"error: {str(e)}"
    
    # Redis 연결 확인
    try:
        r = redis.from_url(settings.REDIS_URL)
        r.ping()
        checks["redis"] = "ok"
    except Exception as e:
        checks["redis"] = f"error: {str(e)}"
    
    all_ok = all(v == "ok" for v in checks.values())
    return {
        "status": "ready" if all_ok else "degraded",
        "checks": checks
    }

Readiness 체크가 실패하면 로드 밸런서가 해당 인스턴스로 트래픽을 보내지 않습니다. 이는 배포 중 무중단 업데이트(Zero-downtime deploy)를 구현할 때 핵심적인 역할을 합니다.

7. Docker 컨테이너화

마지막으로 전체를 Docker 이미지로 패키징합니다.

# Dockerfile
FROM python:3.12-slim AS builder

WORKDIR /app
RUN pip install uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

FROM python:3.12-slim

WORKDIR /app
RUN useradd -m appuser

# 빌더에서 의존성만 복사 (이미지 크기 최소화)
COPY --from=builder /app/.venv /app/.venv
COPY . .

ENV PATH="/app/.venv/bin:$PATH"
USER appuser

EXPOSE 8000

CMD ["gunicorn", "app.main:app", "-c", "gunicorn.conf.py"]

다단계 빌드(Multi-stage build)를 사용하면 최종 이미지에 빌드 도구가 포함되지 않아 이미지 크기를 70% 이상 줄일 수 있습니다. uv sync --frozenuv.lock 파일에 고정된 버전만 설치하므로, 배포마다 의존성이 변경되는 사고를 방지합니다.

# docker-compose.yaml
version: "3.9"
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/mydb
      - REDIS_URL=redis://redis:6379/0
      - SECRET_KEY=${SECRET_KEY}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health/live"]
      interval: 30s
      timeout: 5s
      retries: 3

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 10s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s

volumes:
  pgdata:

요약: 프로덕션 배포 체크리스트

FastAPI를 프로덕션에 배포하기 전 아래 항목들을 모두 확인하세요.

  1. Gunicorn + Uvicorn — 단일 Uvicorn 대신 Gunicorn으로 멀티 워커 관리
  2. Pydantic Settings — 환경변수 타입 안전 관리, 필수값 누락 시 즉시 실패
  3. 비동기 DB 세션pool_pre_ping으로 끊어진 연결 자동 복구
  4. CORS + Rate Limitingslowapi로 엔드포인트별 요청 제한
  5. 보안 헤더 — X-Content-Type-Options, X-Frame-Options 등 필수 헤더 추가
  6. 헬스체크 — Liveness/Readiness 분리, DB·Redis 의존성 확인
  7. Docker 다단계 빌드uv sync --frozen으로 재현 가능한 이미지
  8. 워커 재시작 정책max_requests로 메모리 누수 방지

이 패턴들을 하나씩 적용해 나가면, 개발 중의 빠른 프로토타이핑 장점을 유지하면서도 프로덕션에서 요구되는 안정성과 성능을 동시에 확보할 수 있습니다. 처음부터 모든 걸 다 적용할 필요는 없습니다. 프로젝트 규모와 트래픽에 맞춰 점진적으로 도입하는 것도 좋은 전략입니다.