본문으로 건너뛰기
개발 머꼬
개발 노트Python
hohyeon.dev21

async def를 붙였는데 다른 요청이 전혀 동시 처리되지 않은 이유

  • #asyncio
  • #Concurrency
  • #Engineering Note
  • #Python

문제 발생

FastAPI로 만든 API에 async def를 붙였는데, 요청 하나가 오래 걸리는 동안 다른 요청이 전혀 처리되지 않고 순서대로 밀려서 처리됐습니다 — 비동기의 이점이 전혀 없어 보였습니다.

@app.get("/report")
async def get_report():
    data = requests.get("https://slow-api.example.com")  # 동기 블로킹 호출
    return process(data)

원인 분석

asyncio협력형(cooperative) 멀티태스킹입니다. 하나의 이벤트 루프가 여러 코루틴을 번갈아 실행하는데, 이 "번갈아 실행"이 가능한 건 각 코루틴이 await를 만날 때마다 스스로 실행을 양보(yield)하기 때문입니다.

문제는 requests.get(...) 같은 동기(블로킹) 함수await가 아닙니다 — 이 함수는 응답이 올 때까지 그 스레드 자체를 완전히 멈춰버립니다. 이벤트 루프는 단일 스레드에서 실행되는 경우가 많으므로, requests.get()이 블로킹되어 있는 동안 이벤트 루프 자체가 다른 어떤 코루틴도 진행시킬 수 없습니다 — async def를 붙였어도 사실상 동기 코드와 똑같이 동작하게 됩니다.

해결 방안

  1. 비동기 지원 라이브러리로 교체합니다. requests 대신 httpx(AsyncClient), aiohttp 등 실제로 await 가능한 비동기 HTTP 클라이언트를 씁니다.
import httpx

@app.get("/report")
async def get_report():
    async with httpx.AsyncClient() as client:
        response = await client.get("https://slow-api.example.com")  # 진짜 non-blocking
    return process(response)
  1. 정말 동기 라이브러리를 벗어날 수 없는 경우(레거시 라이브러리, CPU 위주 작업 등)라면, 그 호출을 별도 스레드 풀에서 실행하도록 넘겨 이벤트 루프를 막지 않게 합니다.
import asyncio

@app.get("/report")
async def get_report():
    data = await asyncio.to_thread(requests.get, "https://slow-api.example.com")
    return process(data)
  1. DB 드라이버도 마찬가지입니다 — 동기 드라이버(psycopg2 등)를 async def 안에서 그대로 쓰면 같은 문제가 생깁니다. 비동기 드라이버(asyncpg, aiomysql 등)나 ORM의 비동기 지원 기능을 씁니다.
  2. "이 함수가 실제로 await할 대상이 있는가"를 항상 확인하는 습관이 필요합니다 — async def를 붙이는 것 자체는 아무것도 자동으로 비동기로 만들어주지 않습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.