
FastAPI로 API를 만들다 보면 항상 마주치는 질문이 있습니다. "이 경로 처리 함수, def로 써야 할까 async def로 써야 할까?" 언뜻 사소해 보이지만 이 선택이 서버 성능에 꽤 큰 영향을 줍니다. 공식 문서의 Concurrency and async / await 페이지를 기반으로, FastAPI가 동기와 비동기를 어떻게 구분하고 처리하는지, 그리고 그게 왜 이렇게까지 편하게 느껴지는지 정리해봤습니다.
가장 인상 깊었던 부분은 이거였습니다.
어쨌든 위의 어떤 경우에서도 FastAPI는 여전히 비동기적으로 동작하며 매우 빠릅니다.
즉 def로 짜든 async def로 짜든, 하나의 앱 안에서 두 스타일을 섞어 쓰든 FastAPI가 알아서 올바르게 처리합니다. 개발자가 굳이 이벤트 루프를 직접 관리하거나 콜백 지옥에 빠질 필요가 없다는 뜻입니다. 다만 상황에 맞게 고르면 성능을 더 끌어올릴 수 있죠.
FastAPI 문서가 제시하는 기준은 놀라울 만큼 단순합니다.
await로 호출하라고 안내한다면 → 경로 처리 함수를 async def로 선언한다.@app.get("/")
async def read_results():
results = await some_library()
return results
await를 지원하지 않는다면(대부분의 DB 드라이버가 아직 그렇습니다) → 그냥 일반 def로 선언한다.@app.get("/")
def results():
results = some_library()
return results
def를 쓰면 된다.이게 전부입니다. 코루틴이 뭔지, 이벤트 루프가 어떻게 도는지 몰라도 이 세 줄만 기억하면 실무에서 막힐 일이 거의 없습니다.
비동기가 의미 있는 이유는 결국 "기다림" 때문입니다. 네트워크로 요청을 받고, DB 쿼리 결과를 기다리고, 외부 API 응답을 기다리는 작업들은 CPU 입장에서 보면 대부분 멍하니 대기하는 시간입니다. 이런 작업을 I/O 바운드 작업이라고 부릅니다.
동기 코드는 이 대기 시간 동안 아무것도 못 하고 멈춰 있습니다. 반면 비동기 코드는 "이 작업은 끝날 때까지 시간이 걸리니, 그동안 다른 요청을 처리하고 있다가 결과가 오면 알려줘"라는 식으로 동작합니다. 웹 API는 수많은 요청이 동시에 들어오고 각 요청마다 크고 작은 대기가 끼어 있는 전형적인 I/O 바운드 워크로드라서, 비동기 처리의 이점을 가장 크게 보는 영역입니다.
문서에서는 이걸 햄버거 가게 비유로 설명합니다. 점원 한 명이 주문만 받고 손님은 번호표를 들고 자리에 가서 대화하다가 번호가 뜨면 찾으러 가는 방식(동시성)이, 점원이 요리까지 끝낼 때까지 카운터 앞에서 멀뚱히 서서 기다리는 방식(동기)보다 훨씬 효율적이라는 거죠. 재미있는 비유지만 핵심을 정확히 찌릅니다.
파이썬의 async/await 문법이 좋은 건, 비동기 코드를 마치 순서대로 실행되는 일반 코드처럼 읽고 쓸 수 있게 해준다는 점입니다.
async def get_burgers(number: int):
# 햄버거를 만들기 위한 비동기 처리
return burgers
await get_burgers(2)라고 쓰면 "이 작업이 끝날 때까지 기다렸다가 결과를 받아라, 그동안 파이썬은 다른 일을 하러 가도 된다"는 뜻이 됩니다. 예전처럼 콜백을 겹겹이 쌓거나 스레드를 직접 관리하지 않아도, 코드는 위에서 아래로 읽히는 동기 코드와 똑같이 생겼는데 실제로는 논블로킹으로 동작합니다. 이게 콜백 지옥이나 스레드 동기화 문제를 겪어본 사람이라면 특히 크게 와닿는 편의성입니다.
기술적으로 좀 더 들어가면, FastAPI(정확히는 그 기반인 Starlette)는 경로 처리 함수가 어떻게 선언됐는지에 따라 실행 방식을 다르게 가져갑니다.
async def로 선언한 함수는 이벤트 루프 위에서 직접 실행됩니다.def로 선언한 함수는 서버가 블로킹되지 않도록 별도의 외부 스레드풀에서 실행된 뒤 그 결과를 await 합니다.의존성(Dependency)에도 동일한 규칙이 적용되고, 서로 다른 스타일의 의존성이 얽혀 있어도 FastAPI가 알아서 조율해줍니다. 즉 개발자는 "이 작업이 I/O를 기다리는가, 아닌가"만 판단하면 되고, 실행 위치를 어디로 보낼지는 프레임워크가 대신 결정해줍니다.
문서는 동시성(concurrency)과 병렬성(parallelism)을 구분하는 데도 공을 들입니다. 둘 다 "여러 일이 비슷한 시간에 일어난다"는 점은 같지만,
FastAPI는 웹 API처럼 대기가 많은 상황에서는 비동기(동시성)로, 머신러닝 추론처럼 순수 연산이 많은 CPU 바운드 상황에서는 멀티프로세싱(병렬성)으로 대응할 수 있어서 두 마리 토끼를 다 잡을 수 있다는 점도 인상적이었습니다.
await를 지원하면 async def, 아니면 그냥 def.def를 써도 FastAPI는 여전히 빠르게 동작한다.def와 async def는 한 프로젝트, 심지어 한 라우터 안에서도 섞어 쓸 수 있고 FastAPI가 알아서 처리한다.def는 스레드풀에서, async def는 이벤트 루프에서 실행되도록 FastAPI가 자동으로 라우팅해준다.결국 FastAPI의 async 처리 방식이 매력적인 이유는 어렵게 설계된 게 아니라, "몰라도 동작하고, 알면 더 잘 동작한다"는 점에 있는 것 같습니다.