View

728x90
반응형

"FastAPI는 빠르다"는 말은 이미 수없이 들어봤을 것이다. 그런데 FastAPI가 빠른 이유를 한 줄로 설명해 보라고 하면 대부분 "async라서 빠르다"고 얼버무린다. 이는 반은 맞고 반은 틀린 이야기다. 왜일까? async는 만능 버튼이 아니며, 잘못 쓰면 오히려 sync보다 느려지기 때문이다.

 

이 글에서 다룰 내용은 세 가지다. ① FastAPI를 누가 만들었는지 ② 정확히 무엇을 기반으로 빠른 것인지 ③ 어떤 상황에서는 오히려 느려지는지. 공식 문서 번역글도 아니고, 벤치마크 스크린샷만 던지는 글도 아니다. Python 생태계에서 FastAPI가 어느 포지션에 있는지, 실무에서 async def를 남발하면 왜 실패하는지까지 모두 분석했다.

 

이 글을 읽고 나면 최소한 "FastAPI를 쓰는데 왜 빨라지지 않는가" 같은 삽질은 하지 않게 될 것이다.

 

FastAPI를 만든 사람은 누구인가

 

 

출처: miabajic.dev

Sebastián Ramírez, 콜롬비아 출신의 tiangolo

 

FastAPI를 만든 사람은 Sebastián Ramírez라는 콜롬비아 개발자다. GitHub 핸들은 tiangolo이다. 독일과 스페인에서도 일한 경험이 있으며, 현재는 풀타임 오픈소스 개발자로 활동하고 있다.

 

이 사람이 FastAPI를 만든 이유가 흥미롭다. Flask와 Django를 쓰면서 겪은 불편한 점 세 가지가 겹쳤기 때문이다.

 

  1. 타입 힌트를 활용해 자동 검증을 하고 싶었으나, Flask는 이를 지원하지 못했다.
  2. 자동 문서화(Swagger/OpenAPI)를 수작업 없이 생성하고 싶었다.
  3. 비동기(async/await)를 기본 지원하는 모던 프레임워크가 없었다.

 

이 세 가지를 한 번에 해결하는 프레임워크가 없었기에 직접 만들었다. 2018년 12월 5일에 0.1.0을 공개했다. 그 후 약 2년 만에 GitHub 스타 2만을 기록했고, 2026년 기준으로는 8만+에 도달했다. Flask와 비등하거나 추월했다는 평가가 나올 정도다.

 

tiangolo는 현재 Pydantic 팀에도 속해 있다. FastAPI와 Pydantic은 함께 진화하는 한 몸이라고 봐도 무방하다. 이 사람의 커리어가 곧 Python 백엔드 생태계의 한 축이다.

 

2018년 첫 릴리즈부터 스타 8만까지

 

첫 릴리즈 시점이 2018년 말인데, 이 타이밍이 중요하다. Python 3.6에서 타입 힌트가 제대로 자리잡았고, Python 3.7에서 asyncio가 표준 라이브러리로 안정화됐다. FastAPI는 이 두 가지 위에 정확히 올라탔다. 운도 어느 정도 작용했다고 본다.

 

한국에서도 2020년쯤부터 Django의 대체재로 언급되기 시작했다. 특히 ML 서빙 API를 만들 때 Flask 대신 FastAPI를 쓰는 흐름이 강하다. 이유는 간단하다. 타입 힌트로 모델 입력 검증이 자동화되고, Swagger 문서가 자동 생성되며, 필요할 때 async로 I/O를 처리할 수 있기 때문이다. 세 가지 모두 ML 팀 입장에서 편리한 포인트다.

 

FastAPI가 빠르다는 말의 진짜 의미

 

 

출처: python3.info

FastAPI는 혼자 빠른 것이 아니라 3단 적재 구조다

 

사람들이 착각하는 것이 있다. FastAPI가 무언가 초고성능 엔진을 자체 구현했다고 여기는 것이다. 그렇지 않다. FastAPI는 남이 만든 빠른 라이브러리 세 개를 깔끔하게 묶은 래퍼다. 스택 구조는 다음과 같다.

 

레이어 역할 성능 기여
Uvicorn ASGI 서버 uvloop + httptools로 C 레벨 I/O 처리
Starlette ASGI 웹 프레임워크 라우팅/미들웨어/웹소켓 실질 코어
FastAPI Starlette 위 래퍼 의존성 주입, OpenAPI 자동 생성
Pydantic v2 데이터 검증 Rust로 재작성된 pydantic-core

 

FastAPI가 실제로 하는 일은 이 네 가지 위에서 개발자 경험(DX)을 개선하는 것이다. 라우팅 자체는 Starlette가 처리하고, HTTP 파싱은 Uvicorn이, 검증은 Pydantic이 처리한다. FastAPI는 "여기서 받은 요청을 저기로 넘기고, 타입 힌트를 보고 Pydantic에게 검증을 맡기며, OpenAPI 스키마까지 뽑아내는" 같은 오케스트레이션만 담당한다.

 

따라서 FastAPI가 빠른 이유는 사실 "FastAPI가 빠르다"가 아니라 "FastAPI가 올라탄 세 놈이 모두 빠르다"가 정확한 표현이다.

 

Uvicorn의 uvloop가 진짜 엔진이다

 

Uvicorn이 빠른 이유는 uvloop 때문이다. uvloop는 Node.js가 사용하는 libuv 라이브러리를 Python에서 쓸 수 있게 만든 것이다. libuv는 C로 작성되어 있으며, 비동기 I/O 처리에 최적화되어 있다.

 

숫자로 보면 다음과 같다.

 

  • Python 기본 asyncio 이벤트 루프 대비 2~4배 빠르다(uvloop 공식 벤치 기준).
  • httptools와 조합하면 HTTP 파싱까지 C 레벨에서 처리된다.
  • Go의 net/http에 근접한 수준까지 올라온다(여전히 Go보다는 느리다).

 

Uvicorn을 기본 옵션으로 실행해도 uvloop가 설치되어 있으면 자동으로 감지해 사용한다. pip install uvicorn[standard]로 설치하면 uvloop + httptools가 함께 설치된다. 프로덕션에서 uvicorn[standard]를 설치하지 않는 것은 스스로 발목을 잡는 셈이다.

 

Pydantic v2가 Rust로 동작한다

 

Pydantic은 v1까지는 순수 Python이었다. v2부터 코어를 Rust로 재작성했다. pydantic-core라는 크레이트가 실제 검증 로직을 담당하고, Python 쪽은 그것을 호출하는 얇은 래퍼다.

 

성능 차이는 극단적이다. v1 대비 v2의 성능은 다음과 같다.

 

  • 단순 모델 검증: 5~17배 빠르다.
  • 복잡한 중첩 모델: 최대 50배 빠르다.
  • JSON 직렬화/역직렬화: 약 4배 빠르다.

 

FastAPI는 기본적으로 모든 요청 본문을 Pydantic으로 검증한다. 검증이 빨라지면 전체 API 응답 시간이 단축되는 효과가 크다. 특히 큰 JSON을 받는 엔드포인트에서 차이가 확연히 드러난다.

 

Pydantic v2는 2023년 7월에 안정화됐고, FastAPI는 0.100 버전부터 v2를 기본으로 채택했다. 지금 FastAPI 최신 버전을 쓰면 자동으로 Rust 검증 엔진을 타게 된다.

 

FastAPI 성능 벤치마크를 뜯어봐도 그렇게 압도적이지는 않다

 

 

출처: linkedin.com

TechEmpower에서 Python 최상위, 전체 중 중위권

 

TechEmpower Web Framework Benchmarks는 업계 표준 벤치마크다. Plaintext, JSON, 단일 쿼리, 다중 쿼리, 업데이트, Fortunes 같은 케이스별로 초당 처리 요청 수를 측정한다.

 

FastAPI의 포지션을 요약하면 다음과 같다.

 

  • Python 프레임워크 중에서는 상위권이다. Starlette, FastAPI, BlackSheep, Robyn 같은 현대적 ASGI 기반이 최상위 그룹이다.
  • Django, Flask 같은 WSGI 기반은 같은 Python이어도 훨씬 아래에 있다.
  • 전체 랭킹에서는 중위권이다. Go(fiber, fasthttp), Rust(actix-web, axum), 심지어 Node.js(uWebSockets 기반)에도 뒤진다.
  • 차이가 얼마나 나는가 하면, JSON 벤치 기준 Go/Rust 최상위권이 FastAPI의 3~5배 초당 요청 수를 기록한다.

 

그러므로 "FastAPI는 빠르다"라는 말은 항상 "Python 치고 빠르다"로 번역해야 정확하다. Go와 비교할 급은 아니다. Python이라는 언어 자체가 GIL 때문에 CPU 바운드에서 한계가 있고, 인터프리터 오버헤드도 있어 C/Rust/Go 같은 컴파일 언어에는 구조적으로 이길 수 없다.

 

이것이 FastAPI 성능의 팩트다. Python 생태계 안에서는 최상위권, 전체 언어 비교로는 중위권이다.

 

Starlette 단독이 FastAPI보다 빠르다

 

흥미로운 포인트가 하나 있다. TechEmpower를 돌려 보면 Starlette를 단독으로 쓰는 것이 FastAPI보다 근소하게 빠르다. 왜일까?

 

FastAPI = Starlette + Pydantic 검증 + 의존성 주입 + OpenAPI 생성. 이 "추가 기능들"이 모두 오버헤드다. 특히 Pydantic 검증 비용이 가장 크다. 요청 하나당 모델 인스턴스를 만들고, 필드 검증을 수행하고, 타입 변환을 하는데, 이것이 공짜가 아니다.

 

극한의 성능이 필요하면 선택지가 갈린다.

 

  • Pydantic 검증이 필요하면 FastAPI가 최적이다.
  • 검증이 필요 없고 raw throughput만 중요하면 Starlette를 바로 쓰는 편이 약 10~20% 빠르다.
  • 그것으로도 부족하면 Python을 떠나야 한다(Go, Rust).

 

다만 현실적으로 대부분의 API에서 Pydantic 검증은 필수다. 검증 없이 raw로 받는 것 자체가 보안 리스크이기 때문이다. 따라서 FastAPI 선택이 99% 맞다. "Starlette가 더 빠르다"며 검증을 쓰지 않는 것은 본말전도다.

 

자세한 벤치는 TechEmpower Benchmarks에서 직접 확인할 수 있다.

 

"비동기면 무조건 빠르다"는 헛소리다

 

비동기가 빛나는 순간은 I/O-bound뿐이다

 

FastAPI 비동기 이야기의 핵심은 이것이다. async는 I/O-bound 작업에서만 빛난다.

 

I/O-bound란 무엇인가.

 

  • DB 쿼리를 날리고 결과를 기다리는 경우.
  • 외부 API를 호출하고 응답을 기다리는 경우.
  • 파일 읽기, S3 업로드, Redis 조회.
  • 공통점은 기다리는 시간이 대부분이라는 점이다.

 

이런 작업은 async를 쓰면 기다리는 동안 이벤트 루프가 다른 요청을 처리한다. 1000개의 요청이 각자 DB에서 200ms를 기다려야 해도, async라면 거의 동시에 처리된다. sync라면 1000개를 줄 세워야 한다. 이 경우 성능 차이는 수십 배에 이른다.

 

반대로 CPU-bound 작업은 async를 써도 의미가 없다.

 

  • 이미지 리사이즈/변환.
  • 큰 JSON 파싱.
  • 복잡한 수학 계산.
  • 해시 계산, 암호화.

 

이런 작업은 CPU가 계속 일하는 중이라 async로 감싸도 "다른 요청이 끼어들 틈"이 없다. 오히려 더 나쁘다. 왜일까? async 함수가 CPU를 점유하는 동안 이벤트 루프 전체가 블록되기 때문이다. 다른 모든 요청이 이 하나 때문에 밀린다.

 

async def 안에서 sync를 호출하면 실패한다

 

이것이 FastAPI 실무에서 가장 많이 터지는 함정이다. 코드 예시로 보면 명확하다.

 

@app.get("/data")
async def get_data():
    # 이렇게 쓰면 안 된다. 이벤트 루프 전체가 정지한다.
    response = requests.get("https://api.example.com/data")
    return response.json()

 

async def 안의 requests.get()은 sync 블로킹 호출이다. 이를 만나는 순간 이벤트 루프가 통째로 멈춘다. 다른 요청이 몇 개 대기 중이든 모두 밀려난다. 성능이 sync 엔드포인트보다도 훨씬 나빠진다.

 

같은 방식으로 문제를 일으키는 조합들이다.

 

  • async def + psycopg2(동기 PostgreSQL 드라이버)
  • async def + pymongo(동기 MongoDB 드라이버)
  • async def + time.sleep()(당연하지만 자주 실수하는 부분이다)
  • async def + open()으로 큰 파일 읽기

 

정답은 비동기 드라이버를 쓰는 것이다.

 

  • HTTP: requests → httpx(async 지원)
  • PostgreSQL: psycopg2 → asyncpg 또는 psycopg3 async
  • MongoDB: pymongo → motor
  • 파일 I/O: open() → aiofiles
  • Sleep: time.sleep() → asyncio.sleep()

 

이 조합을 맞춰야 async def의 이득을 본다. 공식 문서에도 async 관련 가이드에서 이 함정을 상세히 설명하고 있다.

 

CPU를 많이 쓰는 작업은 오히려 sync가 낫다

 

FastAPI의 뛰어난 설계 중 하나가 sync 엔드포인트도 그대로 받아준다는 점이다.

 

@app.get("/resize")
def resize_image(url: str):
    # 그냥 def로 정의하면 FastAPI가 스레드풀에서 실행한다.
    image = download(url)
    resized = pil_resize(image)
    return {"ok": True}

 

def(sync)로 정의한 엔드포인트는 FastAPI가 스레드풀에서 실행한다. 이벤트 루프는 다른 요청을 계속 처리하고, 이 CPU 바운드 작업은 별도 스레드에서 돌아간다. GIL은 여전히 존재하지만, 최소한 이벤트 루프를 막지는 않는다.

 

이미지 처리, 수백 MB JSON 파싱, 복잡한 계산 같은 작업은 그냥 def로 쓰는 것이 정답이다. async def로 감싸고 "async니까 빠르겠지"라고 여기는 것이 가장 나쁜 선택이다.

 

헷갈린다면 규칙 하나만 기억하면 된다.

 

  • I/O를 기다리는 작업 + 비동기 드라이버 있음 → async def
  • CPU를 계속 사용하는 작업 → def
  • 모르겠음 → def부터 써도 충분하다

 

실무에서 FastAPI 비동기를 어떻게 써야 하는가

 

 

출처: www.youtube.com

엔드포인트별 def / async def 판단 기준

 

현장에서 바로 써먹을 수 있는 판단 기준을 정리했다.

 

상황 선택 이유
비동기 DB 드라이버(asyncpg) `async def` 진짜 async 이득을 본다
외부 HTTP 호출 많음 + httpx `async def` I/O 병렬 처리로 체감 빠름
SQLAlchemy sync를 쓰는 레거시 `def` 스레드풀이 오히려 안전하다
이미지 처리, ML 추론 `def` CPU-bound, 이벤트 루프 보호
DB 쿼리 한 번 + 단순 리턴 아무거나 차이가 체감되지 않음
잘 모르겠는 신규 프로젝트 `def`부터 나중에 병목이 생기면 바꾸면 된다

 

요점은 async def를 남발하지 말라는 것이다. 전체를 async로 통일하는 것이 깔끔해 보여도, 중간에 sync 블로킹 호출이 하나만 섞여도 전체가 무너진다. 차라리 확실하게 async 생태계(asyncpg, httpx)로 통일하거나, 애매하면 sync로 가는 편이 안전하다.

 

Uvicorn 프로덕션 튜닝 팁

 

로컬에서 uvicorn main:app --reload를 쓰는 것과 프로덕션 설정은 완전히 다르다. 필수 옵션들이다.

 

uvicorn main:app \
  --host 0.0.0.0 \
  --port 8000 \
  --workers 4 \
  --loop uvloop \
  --http httptools

 

  • --workers: 워커 프로세스 수. CPU 코어 수 기준으로 잡는 것이 표준이다. Python은 GIL 때문에 한 프로세스에서 진짜 병렬 처리가 불가능하므로, 프로세스 여러 개를 띄우는 것이 정석이다.
  • --loop uvloop: 명시적으로 uvloop를 지정한다. 기본값이지만 명시하는 편이 안전하다.
  • --http httptools: 파서도 C 기반으로 지정한다.

 

더 제대로 배포하려면 Gunicorn + Uvicorn worker 조합을 자주 쓴다.

 

gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker

 

Gunicorn이 프로세스 관리, 재시작, 헬스체크를 담당하고, 각 워커가 Uvicorn으로 동작한다. 안정성과 성능 둘 다 챙기는 구조다. 자세한 내용은 Uvicorn 공식 문서를 확인하라.

 

추가 팁이다.

 

  • 리버스 프록시(Nginx, Caddy) 앞에 세우는 것은 기본이다.
  • gzip 압축은 Nginx에서 처리하는 편이 Uvicorn에서 처리하는 것보다 빠르다.
  • --access-log는 프로덕션에서 끄거나 JSON으로 포맷팅해 별도로 수집한다.

 

세 줄로 요약하고 마친다

 

FastAPI가 빠른 이유를 최대한 짧게 다시 정리한다.

 

  1. FastAPI는 Ramírez가 2018년에 만든 프레임워크다. Uvicorn + Starlette + Pydantic 위에 올라탔고, 이 세 가지가 각자 빠르기 때문에 FastAPI도 빠르다. 혼자 잘난 것이 아니라 스택이 잘난 것이다.
  2. 빠른 것은 사실이나 "Python 치고 빠름"이 정확하다. Go/Rust를 상대로는 여전히 밀린다. 다만 Python 생태계 안에서는 현시점 최상위권 선택지다.
  3. 비동기를 쓴다고 무조건 빨라지는 것은 아니다. async def 안에서 sync 블로킹 호출이 섞이면 오히려 최악이다. CPU-bound 작업은 그냥 def로 쓰고, I/O-bound + 비동기 드라이버가 있을 때만 async def를 쓰는 것이 정답이다.

 

다음에 FastAPI 프로젝트를 만들 때는 async def를 남발하지 말고, 엔드포인트별로 "이 작업이 I/O를 기다리는 작업인가, CPU를 돌리는 작업인가"부터 따져 보라. 그리고 비동기 드라이버(asyncpg, httpx)를 쓰지 않을 것이라면 차라리 def로 쓰는 편이 낫다. 이 하나만 지켜도 FastAPI 성능을 최대로 뽑아낼 수 있다. "async = 빠름"이라는 미신에서 벗어나는 것이 첫걸음이다.

728x90
반응형
Share Link
reply
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31