타임트리

[FastAPI] 동기, 비동기 테스트 본문

Today I Learned/동시성 프로그래밍

[FastAPI] 동기, 비동기 테스트

sean_j 2025. 2. 10. 02:32

FastAPI에서 알아보는 동기와 비동기 처리의 차이

FastAPI를 통해 동기(Synchronous)와 비동기(Asynchronous) 처리의 차이점에 대해 알아보자. 실제 코드 예제와 함께 이 두 가지 처리 방식의 동작 원리를 자세히 살펴본다.

동기와 비동기의 기본 개념

동기 처리 (Synchronous)

  • 여러 개의 스레드를 사용하여 작업을 병렬(Parallelism)로 처리
  • 각 요청마다 새로운 스레드가 할당됨
    → FastAPI에서는 def로 정의하면 호출 시 내부적으로 ThreadPoolExecutor를 활용해 관리되어 여러 스레드에 동시 처리 가능 (병렬성)

비동기 처리 (Asynchronous)

  • 단일 스레드에서 이벤트 루프를 통해 동시성(Concurrency)을 구현
  • I/O 작업 중에 다른 작업을 처리할 수 있음
  • CPU 자원을 효율적으로 사용

코드로 보는 동기와 비동기 처리

비동기 처리 예제

import asyncio
import threading
from fastapi import FastAPI

app = FastAPI()


async def io_task():
    await asyncio.sleep(5)
    return {"status": "io_task completed"}


@app.get("/task")
async def task():
    print(f"... task 호출 [Thread-{threading.get_ident()}]")
    result = await io_task()
    return result

 

asyncio.sleep()는 CPU를 블로킹하지 않고 다른 작업을 수행할 수 있게 한다.

uvicorn main:app --workers=1 로 하나의 워커 프로세스를 띄우고, 아래처럼 5개의 요청을 보내보면 5개의 요청이 모두 같은 스레드에서 실행되며 (동일 스레드 내 이벤트 루프), 비동기 방식으로 처리되고 있다.

time (curl http://127.0.0.1:8000/task & curl http://127.0.0.1:8000/task & curl http://127.0.0.1:8000/task & curl http://127.0.0.1:8000/task & curl http://127.0.0.1:8000/task)
... run_task 호출 [Thread-139642321622080]
... run_task 호출 [Thread-139642321622080]
... run_task 호출 [Thread-139642321622080]
... run_task 호출 [Thread-139642321622080]
... run_task 호출 [Thread-139642321622080]
{"status":"task completed"}{"status":"task completed"}{"status":"task completed"}{"status":"task completed"}{"status":"task completed"}( curl http://127.0.0.1:8000/task & curl http://127.0.0.1:8000/task & curl  &)  0.00s user 0.00s system 0% cpu 5.006 total

동기 처리 예제

@app.get("/task")
def run_task():
    print(f"... task 호출 [Thread-{threading.get_ident()}]")
    time.sleep(5)
    return {"status": "task completed"}

 

반대로 위의 코드에서는 time.sleep(5)가 CPU를 블로킹한다.

 

비동기 예제와 마찬가지로 uvicorn main:app --workers=1 로 하나의 워커 프로세스를 띄우고, 아래처럼 5개의 요청을 보내보면 5개의 요청이 모두 다른 스레드에서 실행되는 걸 확인할 수 있다.

time (curl http://127.0.0.1:8000/task & curl http://127.0.0.1:8000/task & curl http://127.0.0.1:8000/task & curl http://127.0.0.1:8000/task & curl http://127.0.0.1:8000/task)
... run_task 호출 [Thread-140385687238208]
... run_task 호출 [Thread-140385678845504]
... run_task 호출 [Thread-140385670452800]
... run_task 호출 [Thread-140385662060096]
... run_task 호출 [Thread-140385653667392]
{"status":"task completed"}{"status":"task completed"}{"status":"task completed"}{"status":"task completed"}( curl http://127.0.0.1:8000/task & curl http://127.0.0.1:8000/task & curl  &)  0.00s user 0.00s system 0% cpu 5.011 total

성능 차이와 사용 시나리오

비동기 처리가 유리한 경우

  • I/O 바운드 작업이 많은 경우
    • 데이터베이스 쿼리
    • 외부 API 호출
    • 파일 읽기/쓰기
  • 동시에 많은 연결을 처리해야 하는 경우
  • 메모리 사용량을 최소화해야 하는 경우

동기 처리가 유리한 경우

  • CPU 바운드 작업이 많은 경우
  • 개별 요청의 처리 시간이 매우 짧은 경우

결론

비동기 처리는 이벤트 루프를 통한 동시성을 제공하여 I/O 바운드 작업에서 뛰어난 성능을 보여준다. 반면 동기 처리는 여러 스레드를 통한 병렬 처리로 CPU 바운드 작업에서 장점을 가집니다. 따라서 FastAPI 애플리케이션을 구현할 때 CPU 바운드와 I/O 바운드를 잘 구분하고 상황에 맞게 코드를 작성하는 게 중요하다.