FastAPI — Types Are the Contract
The Leaking Field and the Stalling Loop
한국어 원문으로 표시합니다.
목표
실무에서 FastAPI 로 가장 자주 터지는 두 가지를 직접 일으키고 고칩니다.
- 응답에 비밀 필드가 새는 것 (3 → 4단계)
async def안의 블로킹이 서버 전체를 멈추는 것 (7단계)
규칙
- 파일은 전부
/root/work/api에 만듭니다. - 앱 인스턴스 이름은 반드시
app, 저장소 의존성 이름은get_store로 하세요. 채점기가 그 이름으로 불러옵니다. - 채점기는 여러분의
app.py를 직접 불러와 요청을 넣어 봅니다. uvicorn 이 떠 있지 않아도 채점됩니다(1단계만 예외 — 실제로 띄워야 합니다).
서버 띄우기
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 후 다시 띄우는 편이
무엇이 돌고 있는지 분명합니다.
단계
/healthz→01-healthz.txt- Pydantic 검증, 422 →
02-422.json - 일부러 유출 →
03-leak.json response_model로 차단HTTPException404Depends(get_store)async defvsdef→07-block.txt·07-unblock.txtdependency_overrides테스트- 정리 →
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: str 과 qty: 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 를 만들어 email 과 hashed_password 를 둘 다 가진 객체를 그대로 반환하게 하세요. 응답에 해시가 그대로 나오는 것을 03-leak.json 으로 남깁니다. 이 단계의 정답은 유출입니다.
response_model 없이 dict 나 모델을 그대로 return 하면 전부 나갑니다. 이게 실제로 자주 일어나는 사고이고, 다음 단계에서 막습니다.
출력 모델로 막는다
GET /me/safe 를 추가하고 response_model 로 hashed_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 /slow 는 async def, GET /slow2 는 def. 각각 4개를 동시에 던져 걸린 시간을 07-block.txt 와 07-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_overrides 로 get_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_model 과 def 두 단어가 본문에 들어가야 합니다. 이 둘이 FastAPI 에서 가장 자주 터지는 사고입니다.