Joyuno's Dev Blog

2025년 FastAPI 백엔드 베스트 프랙티스: “빠른 개발”을 “지속 가능한 아키텍처”로 바꾸는 설계법

Backend Tutorial

들어가며

FastAPI는 “타입 힌트 기반의 validation + OpenAPI 자동 생성 + ASGI 성능”이라는 조합 덕분에 2025년에도 마이크로서비스/서버리스/API 전용 백엔드에서 강력한 선택지입니다. 하지만 실무에서는 성능보다 더 자주 “구조가 무너져서 바꾸기 어려움”, “에러 포맷이 제각각”, “Django(모놀리식)와 경계가 불명확” 같은 문제로 고생합니다.
즉, FastAPI는 프레임워크가 가볍기 때문에 아키텍처와 API 설계 규칙을 팀이 직접 고정해야 합니다. 이 글은 최신 문서/표준 흐름(예: OAuth2 scopes, Problem Details, OpenAPI 스펙 업데이트, Pydantic v2 변화)을 바탕으로 2025년형 베스트 프랙티스를 정리합니다. (fastapi.tiangolo.com)


🔧 핵심 개념

1) “FastAPI는 프레임워크”가 아니라 “조립식 플랫폼”

Django/DRF는 ORM, Admin, Auth, settings 패턴까지 강하게 제공하지만, FastAPI는 Starlette 위에서 라우팅/DI/스키마를 제공하는 형태라 경계(레이어)를 안 정하면 곧장 스파게티가 됩니다. 반대로 말하면, 레이어만 잘 나누면 서비스 단위 분리가 쉬워지고 Django와 공존(모놀리식+API 서비스)도 깔끔해집니다. (Django 생태계가 필요한 곳은 Django/DRF, 고성능 API는 FastAPI로 분리하는 패턴이 2025년에도 흔합니다.) (blueshoe.io)

2) Pydantic v2 시대의 “스키마 = 계약(Contract)”

2025년 FastAPI 프로젝트에서 Pydantic v2는 사실상 기본 전제입니다. v2는 model_validate, model_dump 같은 네이밍/동작 변화가 있고(특히 직렬화/보안 관점에서 서브클래스 덤프 동작 변경 등), 이를 이해하지 못하면 “응답에서 필드가 사라짐” 같은 장애로 이어집니다. 즉, request/response 모델을 계약으로 엄격히 유지하고, 내부 도메인 모델과 섞지 않는 것이 중요합니다. (docs.pydantic.dev)

3) API 설계의 표준화: Problem Details(RFC 9457) + OpenAPI 품질

  • 에러 포맷을 팀/서비스별로 다르게 만들면 클라이언트/프론트/앱이 유지보수 지옥이 됩니다. RFC 7807에서 확장된 RFC 9457 Problem Details는 에러 응답을 표준 형태로 고정해 줍니다. (swagger.io)
  • OpenAPI는 2024년 말에 3.0.4/3.1.1 patch 업데이트로 가이드가 보강되었고, Overlay/Arazzo 같은 주변 스펙도 나오며 “문서 = 자동화 파이프라인 자산”으로 보는 흐름이 강화됐습니다. 즉, FastAPI의 자동 OpenAPI를 “그냥 덤”이 아니라 API-first 운영 도구로 다뤄야 합니다. (openapis.org)

4) 인증/인가: OAuth2 scopes를 “DI 그래프”로 모델링

FastAPI는 Depends, Security, SecurityScopes로 인가 요구사항을 의존성으로 표현합니다. 이 방식은 컨트롤러에 if문을 흩뿌리는 것보다 훨씬 테스트 가능하고, OpenAPI에도 정확히 반영됩니다. (fastapi.tiangolo.com)


💻 실전 코드

아래 예시는 “레이어 분리 + Lifespan 리소스 초기화 + Pydantic v2 스키마 + RFC 9457 스타일 에러 + OAuth2 scopes”를 한 번에 보여주는 최소 골격입니다.

from __future__ import annotations

from contextlib import asynccontextmanager
from typing import Annotated, Any

from fastapi import Depends, FastAPI, HTTPException, Request, Security, status
from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from pydantic import BaseModel, ConfigDict

# -----------------------
# API 계약(Contract) 모델: 외부 입력/출력 전용
# -----------------------
class UserOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # Pydantic v2: ORM/속성 기반 변환에 필요
    id: int
    email: str


class ProblemDetails(BaseModel):
    # RFC 9457의 핵심 필드 형태를 따르되, 내부 정책에 맞춰 확장 가능
    type: str
    title: str
    status: int
    detail: str | None = None
    instance: str | None = None
    # 필요하면 errors: dict[str, Any] 같은 확장 필드 추가


# -----------------------
# 인프라/리소스: lifespan으로 앱 생명주기 관리
# (DB pool, HTTP client, cache 연결 등을 여기서 초기화/정리)
# -----------------------
@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.service_name = "accounts-api"
    # 예: app.state.db = await create_pool(...)
    yield
    # 예: await app.state.db.close()


app = FastAPI(lifespan=lifespan)

# -----------------------
# 에러 표준화: Problem Details 형태로 통일
# -----------------------
@app.exception_handler(HTTPException)
async def http_exception_as_problem_details(request: Request, exc: HTTPException):
    body = ProblemDetails(
        type="about:blank",
        title=exc.detail if isinstance(exc.detail, str) else "HTTP Error",
        status=exc.status_code,
        detail=None,
        instance=str(request.url.path),
    )
    return JSONResponse(status_code=exc.status_code, content=body.model_dump(mode="json"))


# -----------------------
# 보안: OAuth2 scopes를 DI로 모델링 (OpenAPI에도 반영)
# -----------------------
oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl="token",
    scopes={
        "users:read": "Read user information",
        "users:admin": "Administrative user actions",
    },
)

# 실제 구현에서는 토큰 검증(JWT decode 등) + scope 검사 + 사용자 로딩이 들어감
async def get_current_user(
    security_scopes: SecurityScopes,
    token: Annotated[str, Depends(oauth2_scheme)],
) -> dict[str, Any]:
    # 주석: security_scopes.scopes에는 라우트/하위 의존성이 요구하는 scope 목록이 누적됨
    if not token:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing token")

    # 데모: 토큰 문자열에 "admin"이 있으면 admin scope 보유로 가정
    token_scopes = ["users:read"] + (["users:admin"] if "admin" in token else [])
    for required in security_scopes.scopes:
        if required not in token_scopes:
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions")

    return {"id": 1, "email": "dev@company.com"}


# -----------------------
# 라우트: 응답은 response_model로 고정(= 계약 유지)
# -----------------------
@app.get(
    "/users/me",
    response_model=UserOut,
)
async def read_me(
    user: Annotated[dict[str, Any], Security(get_current_user, scopes=["users:read"])],
):
    # 내부 dict -> Pydantic v2 모델로 변환(외부 계약)
    return UserOut.model_validate(user)


@app.get("/admin/ping")
async def admin_ping(
    user: Annotated[dict[str, Any], Security(get_current_user, scopes=["users:admin"])],
):
    return {"ok": True, "by": user["email"]}

⚡ 실전 팁

1) “API 스키마(Contract)”와 “도메인 모델”을 분리

  • UserOut, UserCreate, ProblemDetails 같은 외부 계약 모델은 안정적으로 유지하고,
  • 내부 도메인/ORM 모델은 변경 가능하게 둡니다.
    Pydantic v2의 model_validate, from_attributes=True 같은 규칙을 팀 컨벤션에 명시하세요. (docs.pydantic.dev)

2) 에러는 RFC 9457(Problem Details)로 단일 포맷

  • “message, code, errors…”를 서비스마다 다르게 만들지 말고,
  • status/instance/type 중심의 표준 형태로 묶어두면 게이트웨이/클라이언트/모니터링이 단순해집니다. (swagger.io)

3) 인가 로직은 컨트롤러가 아니라 DI 그래프로 Security(get_current_user, scopes=[...]) 패턴은 “요구 권한”이 코드 구조로 드러나고, OpenAPI에도 반영됩니다. 또한 테스트에서 특정 dependency만 갈아끼우기가 쉬워집니다. (fastapi.tiangolo.com)

4) Django와의 역할 분담을 명확히

  • Django/DRF: Admin, 복잡한 도메인, 백오피스, 강한 ORM 중심
  • FastAPI: 고성능 API, BFF, 비동기 I/O(외부 API 집계), 마이크로서비스
    2025년에도 이 분리는 조직/팀 스케일이 커질수록 효과가 큽니다. (codeartisanlab.com)

5) OpenAPI를 “자동 문서”가 아니라 “제품 스펙”으로 운영 OpenAPI는 최근 patch 릴리즈에서 가이드가 보강되고, Overlay/Arazzo 같은 주변 표준도 등장해 “스펙 자동화” 흐름이 강해졌습니다. CI에서 OpenAPI diff를 검사하거나, 변경을 Overlay로 관리하는 식의 운영을 고려할 만합니다. (openapis.org)


🚀 마무리

2025년 FastAPI 베스트 프랙티스의 핵심은 “빠르게 만든 API”를 계약 중심(Contract-first) + 표준화된 에러 + DI 기반 보안 + 명확한 레이어로 “오래 가는 백엔드”로 바꾸는 것입니다. Pydantic v2 변화는 스키마/직렬화의 의미를 더 무겁게 만들었고, Problem Details/RFC 9457 같은 표준은 팀 간 일관성을 크게 올려줍니다. (docs.pydantic.dev)

다음 학습으로는 (1) FastAPI에서 OpenAPI 커스터마이징/버저닝 전략, (2) Django(모놀리식)와 FastAPI(서비스) 간 인증/권한 위임 패턴, (3) 계약 테스트(consumer-driven contract)까지 확장해보면 “아키텍처가 무너지지 않는 API”에 가까워집니다.