20190109

FastAPI 0.30 타입 기반 API

FastAPI 써봐야지 하다가 0.30이 올라온 김에 사이드 API 하나 구현. Flask/aiohttp를 주로 쓰다가 넘어왔는데, 타입힌트 기반 요청 파싱이 자동으로 되는 게 가장 큰 차이. 사용 소감 + 구조 분석 메모.

from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel, Field
from typing import List, Optional

app = FastAPI(title="rdx-api", version="0.1")

class ItemIn(BaseModel):
    name: str = Field(..., min_length=1, max_length=80)
    price: float = Field(..., gt=0)
    tags: List[str] = []
    note: Optional[str] = None

class ItemOut(ItemIn):
    id: int
    class Config:
        orm_mode = True

@app.post("/items/", response_model=ItemOut, status_code=status.HTTP_201_CREATED)
async def create(item: ItemIn, db = Depends(get_session)):
    row = await db.execute(
        "INSERT INTO items(name, price, tags, note) VALUES($1,$2,$3,$4) RETURNING *",
        item.name, item.price, item.tags, item.note
    )
    return row

pydantic이 요청 본문 검증(타입, 길이, 범위)을 자동 처리. marshmallow 같은 걸 따로 붙일 필요가 없다. response_model로 직렬화 스키마도 분리 가능해서 "내부 모델은 넓고, 응답은 제한적으로"가 깔끔하게 표현된다. orm_mode=True로 ORM 객체 직접 반환도 지원.

OpenAPI 자동 생성이 꽤 크다. /docs에 Swagger UI, /redoc에 ReDoc. 타입힌트 + pydantic 모델만으로 스펙이 완성되니까 문서-코드 싱크 문제가 원천 차단. 프런트와 협업할 때 openapi.json을 openapi-generator에 물려 TS 클라이언트 자동 생성. swagger-ui 수동 유지하던 부담이 사라졌다.

내부 구조 훑기

FastAPI는 Starlette 위에 타입힌트 기반 디스패치를 얹은 구조. 요청이 들어오면 Starlette의 ASGI 라우팅이 엔드포인트 함수를 찾고, FastAPI가 함수 시그니처를 inspect해서:

  1. 파라미터가 경로인지({id} 매칭), 쿼리인지, 바디인지, 의존성인지 분류
  2. pydantic 모델은 JSON body로 추정, 기본형은 쿼리 파라미터로 추정
  3. Depends(x)로 선언된 것은 함수 호출 결과를 주입
  4. 모두 검증 통과하면 view 호출, 반환값은 response_model에 맞춰 직렬화

시그니처 인스펙션은 요청마다 반복하면 당연히 비싸니까 앱 기동 시점에 pre-analysis. 라우트 등록 시 ParamField 메타데이터를 구성해 캐싱. 그래서 첫 요청 이후엔 reflection이 일어나지 않는다.

의존성 주입이 특히 깔끔. 인증, DB 세션, 페이지네이션 공통 파라미터 같은 반복 보일러플레이트를 Depends(...)로 빼면 뷰 함수가 본질만 남는다. 의존성 자체도 다른 의존성을 가질 수 있어서 트리 구조. 같은 의존성이 요청 내 여러 자리에서 쓰이면 기본적으로 캐싱(use_cache=True).

벤치 (단순 echo)

wrk -t4 -c100 -d30s, 본문 140 바이트 JSON echo.

  • Flask 1.0 + gunicorn sync 4 worker: ~3,100 rps, p99 62ms
  • Flask + gunicorn + gevent 100 worker: ~5,400 rps, p99 48ms
  • aiohttp 3.5: ~9,100 rps, p99 22ms
  • FastAPI 0.30 + uvicorn(uvloop) 1 worker: ~8,700 rps, p99 24ms
  • FastAPI + uvicorn 4 worker: ~28,000 rps, p99 18ms

단순 echo라서 숫자가 크게 나오지만 DB 붙으면 차이는 거의 DB 쪽에서 결정된다. 실질 이득은 속도보다 "개발 경험 + 스키마 자동화"에 있음. 그리고 async 기반이라 외부 HTTP 많이 치는 엔드포인트에선 gunicorn sync 대비 확연한 처리량 차이.

걸리는 점

  • 0.x 브레이킹. 0.25 → 0.30 사이에 Depends 반환 타입 추론, security scheme 필드 이름 같은 것들이 미묘하게 바뀜. 릴리스 노트 꼭 확인. requirements에 pin 하는 걸 권장
  • pydantic 1.0 아직. 0.x대라 BaseModel의 validator 방식이 달라질 가능성. 지금은 @validator("x", pre=True) 관용구인데 1.0에서 바뀔 조짐
  • sync 라이브러리와 섞기. ORM으로 SQLAlchemy 1.2 쓰면 세션이 sync라 async 뷰에서 블록. 실전에선 asyncpg/databases로 가거나, sync 구간을 run_in_threadpool로 격리. 우리는 databases 0.1대로 갔는데 migrations/유틸은 여전히 sqlalchemy로. 이중 관리 약간 귀찮음
  • 테스트 도구. FastAPI TestClient는 내부적으로 Starlette의 TestClient → requests. 훌륭한데 async 경로 디버깅이 sync wrapping을 한 번 더 타서 traceback이 살짝 길다
  • 중간 마이그레이션. Flask 전체를 FastAPI로 바꿀 이유는 적다. 새 엔드포인트는 FastAPI, 기존은 Flask 유지하고 reverse proxy로 경로 분리 운영이 현실적

종합

프로덕션 전면 적용하기엔 아직 버전이 초기. 0.30 기준 내부 툴, 관리자 API, 신규 microservice 같은 곳부터 경험 쌓기 좋다. 단순한 API 문법이면 pydantic 덕분에 마르셜링 레이어를 따로 쓸 필요가 없어서 코드가 30~40% 짧아지는 체감. 1.0 도달하면 메인 스택 후보로 검토할만함.

댓글 없음: