LabHub
배우기 러닝패스 코스

FastAPI — Types Are the Contract

The Leaking Field and the Stalling Loop

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

실무에서 FastAPI 로 가장 자주 터지는 두 가지를 직접 일으키고 고칩니다.

규칙

서버 띄우기

cd /root/work/api
uvicorn app:app --host 0.0.0.0 --port 8000 > /tmp/uv.log 2>&1 &
curl -s localhost:8000/healthz

--reload 는 쓰지 마세요. 파일을 고쳤으면 kill %1 후 다시 띄우는 편이 무엇이 돌고 있는지 분명합니다.

단계

  1. /healthz01-healthz.txt
  2. Pydantic 검증, 422 → 02-422.json
  3. 일부러 유출 → 03-leak.json
  4. response_model 로 차단
  5. HTTPException 404
  6. Depends(get_store)
  7. async def vs def07-block.txt · 07-unblock.txt
  8. dependency_overrides 테스트
  9. 정리 → 09-notes.md

참고

검증 코드를 손으로 쓰지 마세요. if not isinstance(...) 를 쓰고 있다면 타입 힌트가 할 일을 뺏은 것입니다.

서버를 띄운다

/root/work/api/app.py 에 FastAPI 앱을 만들고 GET /healthz{"status":"ok"} 를 주게 하세요. uvicorn 으로 실제로 띄워서 curl 한 결과를 01-healthz.txt 로 남깁니다.

mkdir -p /root/work/api && cd /root/work/api. app = FastAPI() 는 반드시 이름이 app 이어야 합니다 — 채점기가 그 이름으로 불러옵니다. 띄우기: uvicorn app:app --host 0.0.0.0 --port 8000 > /tmp/uv.log 2>&1 & 그다음 curl -s -i localhost:8000/healthz > 01-healthz.txt.

잘못된 입력을 거절시킨다

POST /items 를 만들고 본문을 Pydantic 모델로 받으세요. 모델에는 name: strqty: int 가 있어야 합니다. qty 에 문자열을 보내 422 를 받아 그 본문을 02-422.json 으로 남기세요.

class ItemIn(BaseModel): name: str; qty: int 그리고 def create(item: ItemIn). 검증 코드는 한 줄도 쓰지 마세요 — 타입이 곧 검증입니다. curl -s -X POST localhost:8000/items -H 'content-type: application/json' -d '{"name":"a","qty":"many"}' > 02-422.json.

비밀 필드를 일부러 흘려 본다

GET /me 를 만들어 emailhashed_password둘 다 가진 객체를 그대로 반환하게 하세요. 응답에 해시가 그대로 나오는 것을 03-leak.json 으로 남깁니다. 이 단계의 정답은 유출입니다.

response_model 없이 dict 나 모델을 그대로 return 하면 전부 나갑니다. 이게 실제로 자주 일어나는 사고이고, 다음 단계에서 막습니다.

출력 모델로 막는다

GET /me/safe 를 추가하고 response_modelhashed_password응답에서 사라지게 하세요. 핸들러는 여전히 전체 객체를 반환해도 됩니다.

출력 전용 모델(UserOut)에는 email 만 둡니다. @app.get("/me/safe", response_model=UserOut). 핸들러 코드를 안 고쳐도 필드가 걸러지는 것이 핵심입니다 — 입력 모델과 출력 모델을 절대 같은 클래스로 쓰지 않습니다.

없는 것에는 404 를 준다

GET /items/{item_id} 를 만들어 없는 id 에는 404 와 함께 사람이 읽을 수 있는 메시지를 주게 하세요.

raise HTTPException(status_code=404, detail="..."). return {"error": ...} 로 200 을 주면 안 됩니다 — 상태 코드가 계약의 일부입니다.

의존성으로 저장소를 주입한다

Depends 로 공용 저장소를 주입하도록 바꾸세요. 저장소를 만드는 함수 이름은 get_store 로 합니다.

def get_store(): ... 를 만들고 핸들러에 store = Depends(get_store) 로 받습니다. 전역 변수를 직접 참조하지 마세요 — 다음 단계에서 이걸 통째로 갈아 끼웁니다.

이벤트 루프를 멈춰 보고 고친다

똑같이 time.sleep(0.5) 를 하는 두 경로를 만드세요 — GET /slowasync def, GET /slow2def. 각각 4개를 동시에 던져 걸린 시간을 07-block.txt07-unblock.txt 에 남깁니다. 앞은 2초, 뒤는 1초 안이어야 합니다.

측정: time (for i in 1 2 3 4; do curl -s localhost:8000/slow & done; wait) 2>&1 | tee 07-block.txt. 코드가 글자 하나(async) 빼고 똑같은데 4배가 차이납니다 — 앞은 이벤트 루프 하나에 줄을 선 것이고, 뒤는 FastAPI 가 스레드풀로 보낸 것입니다. 확신이 없으면 def 로 쓰는 편이 안전하다는 것이 이 실습의 결론입니다.

의존성을 갈아 끼워 테스트한다

test_app.py 를 쓰고 dependency_overridesget_store 를 가짜로 바꾼 테스트를 최소 하나 포함하세요. pytest -q 가 통과해야 합니다.

from fastapi.testclient import TestClient, app.dependency_overrides[get_store] = lambda: {...}. 프로덕션 코드를 한 줄도 안 고치고 바꿀 수 있다는 것이 Depends 의 진짜 값어치입니다. 몽키패치는 쓰지 마세요.

두 사고를 요약한다

09-notes.md 에 세 줄. (1) 3단계에서 무엇이 샜는지 (2) 7단계에서 왜 4배가 차이났는지 (3) 각각을 무엇으로 막았는지.

response_modeldef 두 단어가 본문에 들어가야 합니다. 이 둘이 FastAPI 에서 가장 자주 터지는 사고입니다.