본문으로 건너뛰기
Sourcevelog.io

Fastapi 모범사례 15선

· 약 5분
최재훈
LEAD (AI Research Engineer), Brain Crew

TL;DR

FastAPI를 프로덕션 환경에서 안정적으로 운영하기 위한 15가지 핵심 모범 사례를 다룹니다. 블로킹 작업 시 동기 함수 사용, 비동기 라이브러리 활용, 무거운 작업의 분리, 보안을 위한 문서 노출 제한, 구조화된 로깅, 그리고 Gunicorn+Uvicorn 조합의 배포 전략까지 실무에서 즉시 적용 가능한 패턴을 제시합니다. 특히 async/await의 올바른 사용과 의존성 주입을 통한 코드 재사용성 향상이 핵심입니다.

Key Takeaways

  • async/def 선택 기준: 블로킹 작업(파일 I/O, 동기 DB 쿼리)은 def로, 논블로킹 작업은 async def로 정의하여 FastAPI가 적절한 스레드 풀에서 처리하도록 설계
  • ML 추론 작업 분리: CPU/GPU 집약적인 작업은 Triton, TensorFlow Serving 등 전용 엔진으로 오프로드하고, FastAPI는 요청 검증과 라우팅만 담당
  • 의존성 주입 활용: DB 연결, 인증, 검증 로직을 의존성으로 분리하여 코드 재사용성을 높이고 테스트 용이성 확보
  • 보안 강화: 프로덕션에서 Swagger/ReDoc 비활성화, env 파일로 민감 정보 관리, Pydantic Settings로 설정 검증
  • 구조화된 로깅: structlog/loguru로 request_id 기반 컨텍스트 로깅을 구현하여 분산 환경에서 추적 가능성 확보

상세 내용

비동기 처리의 올바른 이해

FastAPI에서 async defdef의 선택은 성능에 직접적인 영향을 미칩니다. async 엔드포인트는 메인 스레드에서 실행되므로, 블로킹 작업(time.sleep(), requests.get(), 동기 DB 클라이언트)을 포함하면 전체 서버가 멈추게 됩니다.

블로킹 작업 처리 방법:

# ❌ 잘못된 방법 - 서버가 10초간 멈춤
@app.get("/")
async def endpoint():
time.sleep(10)

# ✅ 올바른 방법 - Thread Pool에서 처리
@app.get("/")
def endpoint():
time.sleep(10)

반대로 비동기 지원 라이브러리를 활용하면 더 높은 동시성을 달성할 수 있습니다:

# ✅ 비동기 라이브러리 활용
async def endpoint():
await asyncio.sleep(1)
async with httpx.AsyncClient() as client:
await client.get(url)
client = AsyncIOMotorClient()
await client.db.collection.find_one()

무거운 작업의 아키텍처 분리

머신러닝 추론: NVIDIA Triton, TensorFlow Serving, TorchServe 같은 전용 추론 엔진을 사용하면 FastAPI는 경량화된 API 게이트웨이 역할만 수행합니다. 이는 GPU 자원을 효율적으로 사용하고, 추론 서버의 독립적인 스케일링을 가능하게 합니다.

오래 걸리는 작업: Queue + Worker 패턴으로 비동기 처리를 구현합니다:

  1. FastAPI → 메시지 큐(Redis, RabbitMQ)로 작업 전달
  2. 별도 워커가 큐에서 작업 처리
  3. 결과를 DB에 저장
  4. 클라이언트는 polling 또는 webhook으로 결과 조회

간단한 백그라운드 작업:

@app.post("/register")
async def register_user(user_data: UserCreate, bg_tasks: BackgroundTasks):
bg_tasks.add_task(send_email, user_data.email)
bg_tasks.add_task(event_user_registered, user_data.email)
return {"message": "OK"}

⚠️ BackgroundTasks는 서버 재시작 시 작업이 손실되므로, 중요한 작업은 메시지 큐를 사용하세요.

의존성 주입 패턴

의존성 주입은 1-3번 규칙이 동일하게 적용되며, 코드 재사용성을 크게 향상시킵니다.

DB 연결 관리:

@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.pool = await create_pool()
yield
await app.state.pool.close()

async def get_conn(request: Request):
async with request.app.state.pool.acquire() as conn:
yield conn

@app.get("/data")
async def endpoint(db_conn = Depends(get_conn)):
# 커넥션 풀 재사용

검증 로직 분리:

async def validate_owner(
post_id: int,
user = Depends(get_user)
):
post = await db.get(post_id)
if post.user_id != user.id:
raise HTTPException(403)
return post

@app.put("/posts/{post_id}")
async def update_post(post = Depends(validate_owner)):
# 검증 로직 재사용

Pydantic 기반 검증 및 설정 관리

커스텀 Base 모델:

class CustomBaseModel(BaseModel):
class Config:
alias_generator = to_camel
populate_by_name = True
json_encoders = {
datetime: datetime.isoformat,
Decimal: str,
ObjectId: str
}

Validator 활용:

class UserIn(BaseModel):
email: EmailStr
age: int = Field(gte=18)

@validator("email")
def must_be_corporate(cls, v):
if not v.endswith("@company.com"):
raise ValueError("Must be a company email")
return v

응답 자동 검증:

# ✅ FastAPI가 자동으로 검증 및 직렬화
@app.get("/user", response_model=UserOut)
async def get_user():
return {"id": 1, "name": "Alice"} # dict 반환만으로 충분

설정 관리:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
db_url: str
api_key: str
debug: bool

settings = Settings() # .env 자동 로드

보안 강화

문서 노출 제한:

app = FastAPI(
docs_url=None if PRODUCTION else "/docs",
redoc_url=None if PRODUCTION else "/redoc",
openapi_url=None if PRODUCTION else "/openapi.json",
)

프로덕션에서 API 문서를 노출하면 공격자에게 엔드포인트 구조, 파라미터 형식, 인증 방식을 알려주는 것과 같습니다.

환경 변수 관리:

# .env
API_KEY="secret_key"
DATABASE_URL="postgresql://..."

# .gitignore
.env

# .env.example (템플릿)
API_KEY="your_api_key_here"
DATABASE_URL="your_database_url"

구조화된 로깅

structlog 미들웨어:

class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
structlog.contextvars.clear_contextvars()
structlog.contextvars.bind_contextvars(
request_id=str(uuid.uuid4())
)
logger.info("request_received",
method=request.method,
path=request.url.path
)
response = await call_next(request)
logger.info("request_completed",
status_code=response.status_code)
return response

로그 출력 예시:

{
"method": "GET",
"path": "/hello",
"event": "request_received",
"request_id": "d22f859d-443d-9a19-5b8073dd27b6",
"level": "info",
"timestamp": "2025-06-28T17:13:08.501954Z"
}

분산 환경에서는 FileBeat → Elasticsearch → Kibana 같은 중앙 로깅 시스템을 구축하여 여러 인스턴스의 로그를 추적할 수 있습니다.

프로덕션 배포 전략

Gunicorn + Uvicorn 조합:

# 개발 환경
uvicorn main:app --reload

# 프로덕션 환경
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000

uvloop 성능 향상: Node.js의 libuv 기반 고성능 이벤트 루프를 제공합니다. 설치만 하면 FastAPI가 자동으로 감지하여 사용합니다.

Worker 수 결정: 일반 공식은 (CPU 코어 * 2) + 1이지만, 부하 테스트를 통해 최적값을 찾는 것이 중요합니다. I/O 바운드 애플리케이션은 더 많은 워커가, CPU 바운드는 적은 워커가 유리할 수 있습니다.

Docker 컨테이너화: Docker를 사용하면 일관된 환경 보장, 쉬운 스케일링, Kubernetes 같은 오케스트레이션 도구와의 통합이 가능합니다.

References