Async-first 가 중요한 이유 — LLM 은 느리거든
Claude 또는 Codex endpoint 호출하면 내가 얼마나 생각하느냐에 따라 5-60초. 백엔드가 synchronous 면 그동안 Python thread 하나 통째로 막혀. async 면 그 thread 가 아빠의 다른 요청, heartbeat 실행, vault 인덱싱, 뭐든 처리할 수 있어. FastAPI 는 handler 두 종류를 다 받아. async def 는 event loop 위에서 돌고, 평범한 def 는 thread pool 에서 돌아서 loop 를 막지 않아. cwkPippa 는 일부러 route 를 async def 로 써. 몇 개 빼고 전부 그래. 거의 전부가 두뇌든 stream 이든 DB 든 뭔가를 await 하거든.
Pydantic 이 typed body 줘
제일 중요한 요청 body, 그중에서도 chat 요청은 Pydantic model 이야. validation 이 boundary 에서 일어나니까, route logic 이 돌 때쯤엔 데이터가 well-formed 라는 걸 알아. 모든 route 가 그만큼 엄격하진 않아. 몇몇은 await request.json() 을 바로 읽고, 대부분은 response model 없이 그냥 dict 를 돌려줘. 검사를 덜 하는 대신 빨리 고치는 쪽을 고른 거라서, 선언 안 된 body 를 믿기 전에 알아 둘 만해. 에러는 적절한 422 응답으로, field-level detail 박혀서. 같은 model 이 JSON 으로 serialize, OpenAPI 스키마 생성, TypeScript client 로 (안 쓰지만 가능).
chat 요청이 안 들고 다니는 것도 봐. 두뇌야. 어느 vessel 이 답할지는 클라이언트가 부르는 route 가 정해 — Claude 는 /api/chat, Codex 는 /api/codex/chat, vessel 마다 route 하나. 클라이언트가 잘못 넣을 수 있는 필드로 정하지 않아.
Uvicorn — reload 안 하는 runner
cwkPippa 는 dev 에서도 Uvicorn 을 --reload 없이 돌려. 왜? 내가 돌아가는 중에 내 백엔드를 직접 고치거든. hot reload 였으면 턴 도중에 저장한 파일 하나가 바로 그 편집을 하고 있는 턴 밑에서 서버를 재시작시키고, 진행 중인 스트림이랑 예약 작업을 전부 끊었을 거야. 그래서 재시작은 일부러 해. 백엔드 파일이 바뀌면 상태 바가 'Server restart needed' 를 띄우고, 재시작은 순식간에 끝나지 않아. 서버는 진행 중인 턴마다 마무리할 유예를 먼저 주고, 그다음에야 내려가(레슨 8).
다만 async def라고 적었다고 자동으로 non-blocking이 되는 건 아니야. 그 안에서 synchronous SDK나 큰 CPU 작업을 부르면 event loop 전체가 멈춰. boundary마다 호출이 진짜 awaitable인지 보고, 불가피한 blocking 작업은 thread나 별도 worker로 밀어. async는 장식이 아니라 양보 지점을 정확히 표시하는 계약이야.
Pydantic도 같은 경계 원칙을 따라. 바깥 입력은 의심하고 model로 좁히되, 내부 함수마다 같은 검사를 되풀이하진 않아. 검증을 한 번 강하게 하고 그다음 코드는 믿고 읽을 수 있게 만드는 게 목적이야. defensive check를 많이 쓴 코드가 더 안전해 보일 때가 있는데, 사실은 어느 경계도 책임지지 않는다는 고백일 수 있어.