Published on

FastAPI와 asyncio: 비동기 처리 완전 정복

🐍 FastAPI와 asyncio: 비동기 처리 완전 정복

FastAPI는 요즘 Python 웹 개발에서 가장 많이 쓰이는 프레임워크 중 하나다.
나는 오늘 FastAPI가 어떻게 요청을 처리하는지 궁금해서 직접 실험해봤고, 비동기(async)동기(sync) 의 처리 방식 차이를 완벽히 이해할 수 있었다.
특히, FastAPI가 동기 함수도 병렬 처리하는 내부 구조를 보고 정말 흥미로웠다.

✅ FastAPI는 왜 비동기 프레임워크인가?

FastAPI는 ASGI(Asynchronous Server Gateway Interface) 기반이다.
ASGI 덕분에 FastAPI는 async def로 작성한 비동기 엔드포인트를 단일 스레드 이벤트 루프에서 처리할 수 있다.

🧠 asyncio와 이벤트 루프 이해하기

🌀 코루틴(Coroutine)이란?

코루틴은 실행 중간에 멈췄다가 다시 이어서 실행할 수 있는 함수다.
예제 코드를 보자.

@app.get("/asyncio-test/{user_id}")
async def asyncio_test(user_id: int):
    print(f"▶️ User {user_id} START")
    await asyncio.sleep(3)
    print(f"✅ User {user_id} END")
    return {"user": user_id, "status": "done"}

여기서 await asyncio.sleep(3) 구문은 단순한 딜레이가 아니다. 이벤트 루프가 이 코루틴을 일시 중단하고 다른 요청들을 처리한다. 3초 후에 다시 이 코루틴으로 돌아와 이어서 실행한다.

🏎️ 실제 I/O 작업도 동일

asyncio.sleep(3) 대신 API 호출, DB 쿼리, 파일 읽기 등 실제 I/O 작업도 같은 방식으로 처리된다.

async def fetch_data():
    print("📡 서버 요청 시작")
    async with aiohttp.ClientSession() as session:
        async with session.get("https://api.example.com/data") as resp:
            result = await resp.text()
    print("📡 서버 응답 완료")
    return result

여기서도 API 응답을 기다리는 동안 이벤트 루프는 다른 요청들을 실행한다.

syncio 이벤트 루프 동작 다이어그램

+--------------------+
|    이벤트 루프      |
+--------------------+
     ↑        ↑
     │        │
 [코루틴 A] [코루틴 B]
     ↓        ↓
+----+----+  +----+----+
| await I/O|  | await DB|
+---------+  +---------+

🧵 그렇다면 동기(sync) 함수는?

다음은 동기(sync) 엔드포인트이다.

@app.get("/sync-test/{user_id}")
def sync_test(user_id: int):
    print(f"▶️ User {user_id} START")
    time.sleep(3)  # 동기 블로킹
    print(f"✅ User {user_id} END")
    return {"user": user_id, "status": "done"}

만약 이 동기 함수를 이벤트 루프에서 그대로 실행하면, 한 요청이 블로킹될 때 모든 요청이 멈춘다. FastAPI는 이 문제를 해결하기 위해 ThreadPoolExecutor를 사용한다.


🌱 ThreadPoolExecutor 내부 처리

  • 동기 함수는 이벤트 루프에서 실행하지 않고 스레드 풀로 위임된다.
  • 각 요청은 스레드 풀의 스레드 하나를 차지한다.
  • 기본 max_workers는 CPU 코어 수 × 5
  • 요청이 스레드 풀 한계를 초과하면 초과 요청은 대기 상태

🔥 테스트: 비동기 vs 동기 요청 처리 시간

📝 테스트 환경

  • 요청 개수: 100개 동시 요청
  • 서버: Uvicorn (workers=1)
  • 엔드포인트: 위의 asyncio-testsync-test

테스트 앱(FastAPI, app.py)

from fastapi import FastAPI
import asyncio
import time

app = FastAPI()


@app.get("/asyncio-test/{user_id}")
async def asyncio_test(user_id: int):
    print(f"▶️ User {user_id} START - {time.strftime('%X')}")
    await asyncio.sleep(3)  # 3초 동안 대기
    print(f"✅ User {user_id} END - {time.strftime('%X')}")
    return {"user": user_id, "status": "done"}


@app.get("/sync-test/{user_id}")
def sync_test(user_id: int):
    print(f"▶️ User {user_id} START - {time.strftime('%X')}")
    time.sleep(3)  # 동기 블로킹
    print(f"✅ User {user_id} END - {time.strftime('%X')}")
    return {"user": user_id, "status": "done"}

앱서버 실행

uvicorn app:app --reload --workers 1

🏃‍♂️ 테스트 스크립트 (test.sh)

#!/bin/bash

# 테스트할 FastAPI 서버 주소
BASE_URL="http://127.0.0.1:8000"

# 요청 개수
NUM_REQUESTS=100

# ========================
# 1. Async 엔드포인트 테스트
# ========================
echo "🚀 Testing ASYNC endpoint with $NUM_REQUESTS requests..."
START_TIME=$(date +%s)

for i in $(seq 1 $NUM_REQUESTS); do
    curl -s "$BASE_URL/asyncio-test/$i" > /dev/null &
done

wait
END_TIME=$(date +%s)
ASYNC_DURATION=$((END_TIME - START_TIME))
echo "✅ ASYNC completed in $ASYNC_DURATION seconds"


# ========================
# 2. Sync 엔드포인트 테스트
# ========================
echo ""
echo "🚀 Testing SYNC endpoint with $NUM_REQUESTS requests..."
START_TIME=$(date +%s)

for i in $(seq 1 $NUM_REQUESTS); do
    curl -s "$BASE_URL/sync-test/$i" > /dev/null &
done

wait
END_TIME=$(date +%s)
SYNC_DURATION=$((END_TIME - START_TIME))
echo "✅ SYNC completed in $SYNC_DURATION seconds"


# ========================
# 3. 결과 비교
# ========================
echo ""
echo "======================="
echo "🏁 TEST RESULTS"
echo "======================="
echo "ASYNC total time: ${ASYNC_DURATION}s"
echo "SYNC  total time: ${SYNC_DURATION}s"

⏱️ 결과

엔드포인트처리 시간처리 방식
asyncio-test약 3초이벤트 루프에서 코루틴 번갈아 실행
sync-test약 9초ThreadPoolExecutor 스레드 배치 처리
🚀 Testing ASYNC endpoint with 100 requests...
ASYNC completed in 3 seconds

🚀 Testing SYNC endpoint with 100 requests...
SYNC completed in 9 seconds

=======================
🏁 TEST RESULTS
=======================
ASYNC total time: 3s
SYNC  total time: 9s

📦 핵심 요약

구분비동기(async)동기(sync)
처리 방식이벤트 루프 + 코루틴ThreadPoolExecutor로 스레드 실행
I/O 대기 중다른 코루틴 실행다른 스레드 처리 가능
동시 처리 한계없음 (I/O 바운드에 최적)스레드 풀 크기 한계 있음
CPU 바운드 처리GIL로 병렬 불가 (멀티프로세스 필요)스레드 풀 덕분에 어느 정도 가능

내부구조

                 +------------------+
Incoming Request →| ASGI 서버(Uvicorn)|
                 +------------------+
          ┌───────────────┴───────────────┐
          │                               │
   async def 처리                  def 처리
(이벤트 루프 직접 실행)       (ThreadPoolExecutor 위임)

🧠 오늘의 깨달음

✅ FastAPI는 비동기 프레임워크지만 동기 함수도 안전하게 처리한다. ✅ 하지만 동기 함수는 스레드 풀 크기에 따라 병목이 생길 수 있다. ✅ I/O 바운드 작업은 비동기로 작성하는 것이 훨씬 효율적이다. ✅ CPU 바운드 작업은 멀티프로세스 처리가 필요하다.