FastAPI를 실무 흐름으로 이해하기
FastAPI로 타입 안전한 REST API를 빠르게 만듭니다. 라우팅, Pydantic 모델, 의존성 주입, 동기/비동기 처리, 전역 예외 처리, LLM 토큰 스트리밍, 테스트, 배포까지 실무 흐름으로 정리했습니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
FastAPI로 타입 안전한 REST API를 빠르게 만듭니다. 라우팅, Pydantic 모델, 의존성 주입, 동기/비동기 처리, 전역 예외 처리, LLM 토큰 스트리밍, 테스트, 배포까지 실무 흐름으로 정리했습니다.
FastAPI로 타입 안전한 REST API를 빠르게 만듭니다. 라우팅, Pydantic 모델, 의존성 주입, 동기/비동기 처리, 전역 예외 처리, LLM 토큰 스트리밍, 테스트, 배포까지 실무 흐름으로 정리했습니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
문법보다 요청이 들어와 검증, 처리, 저장, 응답으로 이어지는 경계를 먼저 잡습니다.
글로 읽은 내용을 머릿속에 오래 남기려면 먼저 흐름을 그림으로 잡는 편이 좋습니다. 아래 두 그림은 FastAPI를 학습할 때 계속 되돌아볼 수 있는 기준 지도입니다.
FastAPI를 처음 펼칠 때는 세부 명령보다 큰 그림이 먼저입니다. 이 섹션에서는 앞으로 배울 개념들이 어떤 문제를 풀기 위해 등장했는지부터 잡아봅니다.
| 특징 | 설명 |
|---|---|
| 타입 기반 개발 | Python 타입 힌트와 Pydantic 모델로 요청/응답을 검증합니다. |
| 자동 문서화 | OpenAPI, Swagger UI, ReDoc 문서가 자동 생성됩니다. |
| 비동기 지원 | async/await 기반 I/O 작업을 자연스럽게 처리합니다. |
| 테스트 친화성 | TestClient로 API 동작을 빠르게 검증할 수 있습니다. |
여기서는 프로젝트 설정을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
uv venv
source .venv/bin/activate
uv pip install fastapi uvicorn[standard] pydantic
mkdir -p app
touch app/main.py여기서는 라우팅을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from fastapi import FastAPI
app = FastAPI(title="AI DevOps API")
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.get("/users/{user_id}")
def get_user(user_id: int, active: bool = True) -> dict:
return {"id": user_id, "active": active}여기서는 Pydantic 스키마을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from pydantic import BaseModel, Field
class CreateItem(BaseModel):
name: str = Field(min_length=2, max_length=80)
price: float = Field(gt=0)
class Item(CreateItem):
id: int
@app.post("/items", response_model=Item)
def create_item(payload: CreateItem) -> Item:
return Item(id=1, **payload.model_dump())여기서는 의존성 주입을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from fastapi import Depends, Header, HTTPException
async def require_token(authorization: str | None = Header(default=None)) -> str:
if authorization != "Bearer test-token":
raise HTTPException(status_code=401, detail="Unauthorized")
return authorization
@app.get("/me")
def me(token: str = Depends(require_token)) -> dict[str, str]:
return {"token": token}여기서는 동기 vs 비동기 처리을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
import httpx
import requests # 동기 라이브러리
# 잘못된 예 — async def 안에서 동기 호출 → 이벤트 루프 전체 블로킹
@app.get("/bad")
async def bad_endpoint():
response = requests.get("https://api.example.com/data") # 여기서 서버 전체가 멈춤
return response.json()
# 올바른 예 — async 클라이언트로 논블로킹 호출
@app.get("/good")
async def good_endpoint():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/data")
return response.json()| 선언 | 실행 위치 | 주의할 점 |
|---|---|---|
| def 핸들러 | 별도 스레드 풀 | CPU 바운드 작업이 많으면 스레드 풀 자체가 병목이 될 수 있음 |
| async def + await | 메인 이벤트 루프 | 내부에서 반드시 async 라이브러리만 호출해야 함 (httpx, asyncpg 등) |
| async def + 동기 호출 | 메인 이벤트 루프 | 이벤트 루프 전체가 멈춤 — 가장 흔한 성능 사고 |
여기서는 전역 예외 처리을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from fastapi import Request
from fastapi.responses import JSONResponse
class ItemNotFoundError(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request: Request, exc: ItemNotFoundError):
return JSONResponse(
status_code=404,
content={"error": "ITEM_NOT_FOUND", "item_id": exc.item_id},
)
@app.get("/items/{item_id}")
def get_item(item_id: int) -> dict:
if item_id not in db:
raise ItemNotFoundError(item_id) # 핸들러에서 형식만 결정, 여기선 그냥 raise
return db[item_id]여기서는 LLM 토큰 스트리밍을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from fastapi.responses import StreamingResponse
import asyncio
async def generate_tokens(prompt: str):
tokens = ["안녕", "하세요", "!", " 무엇을", " 도와드릴까요", "?"]
for token in tokens:
yield token
await asyncio.sleep(0.1) # 실제로는 LLM API의 스트리밍 청크 도착 시점
@app.post("/chat")
async def chat(prompt: str):
return StreamingResponse(generate_tokens(prompt), media_type="text/event-stream")여기서는 API 테스트을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health():
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}여기서는 실행 & 배포을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
# 개발 실행
uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
# 문서 확인
# http://127.0.0.1:8000/docs
# http://127.0.0.1:8000/redocFastAPI 실무 설계은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 결정 지점 | 확인 질문 | 실무 기준 |
|---|---|---|
| 경계 | FastAPI 코드에서 바뀌기 쉬운 부분은 어디인가? | 입출력, 설정, 외부 연동, 핵심 규칙을 분리합니다. |
| 상태 | 상태가 어디서 생성되고 어디서 사라지는가? | 상태 소유자와 수명 주기를 코드로 드러냅니다. |
| 장애 | 실패했을 때 호출자는 무엇을 받는가? | timeout, fallback, error contract를 먼저 정합니다. |
이 섹션은 FastAPI 운영 기준을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.
FastAPI 검증 전략은 선택지가 갈리는 지점입니다. 표를 기준으로 각 방법의 쓰임새와 운영상의 차이를 비교해두면 이후 판단이 훨씬 쉬워집니다.
| 품질 축 | 검증 방법 | 완료 기준 |
|---|---|---|
| 정확성 | 정상/실패 케이스를 자동화합니다. | 핵심 시나리오가 재현 가능하게 통과합니다. |
| 회귀 방지 | 버그 수정 시 동일 케이스를 테스트로 남깁니다. | 같은 장애가 다시 배포되지 않습니다. |
| 운영성 | 로그, 메트릭, 알림을 확인합니다. | 문제가 생겼을 때 원인 추적 경로가 있습니다. |