Python

FastAPI로 파이썬 비동기 IO 완전 정복: 고성능 백엔드 개발의 비밀

드리프트2 2025. 3. 15. 19:56

FastAPI로 파이썬 비동기 IO 완전 정복: 고성능 백엔드 개발의 비밀

파이썬은 인터프리터 언어이기 때문에 Django와 같은 전통적인 프레임워크로 백엔드를 구축할 때 Java + Spring에 비해 응답 시간이 길 수 있습니다.

하지만 비동기 프레임워크 FastAPI를 사용하면 I/O 집약적 작업의 병렬 처리 능력을 극적으로 향상시킬 수 있는데요.

FastAPI는 현재 파이썬 생태계에서 가장 빠른 프레임워크 중 하나로 꼽힙니다.


예제 1: 기본 네트워크 비동기 IO

설치:

pip install fastapi uvicorn

 

서버 코드:

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def read_root():
    return {"Hello": "World"}

 

실행:

uvicorn main:app --reload

 

async 키워드만 추가하면 일반 함수를 비동기 엔드포인트로 변환할 수 있습니다.

FastAPI는 네트워크 I/O 발생 시 스레드가 대기하지 않고 다른 요청을 처리하다가 I/O 완료 후 작업을 재개하는데요.

이 방식이 I/O 집약적 작업의 처리량을 획기적으로 개선합니다.


예제 2: 명시적 네트워크 비동기 요청

from fastapi import FastAPI, HTTPException
import httpx

app = FastAPI()

@app.get("/external-api")
async def call_external_api():
    async with httpx.AsyncClient() as client:
        response = await client.get("https://leapcell.io")
        if response.status_code != 200:
            raise HTTPException(status_code=500)
        return response.json()

 

데이터베이스 I/O를 비동기로 처리하려면 비동기 지원 ORM (예: SQLAlchemy 1.4+ 또는 Tortoise ORM)이 필요합니다.


비동기 I/O의 핵심 메커니즘

코루틴(Coroutine)

async def fetch_data():
    await asyncio.sleep(1)  # I/O 작업 모방
    return {"data": "result"}
  • async def: 코루틴 함수 선언
  • await: I/O 지점 표시 → 실행 스레드가 다른 작업으로 전환
  • 실행 흐름 제어권 반환이 핵심 원리입니다.

이벤트 루프(Event Loop)

import asyncio

async def main():
    task1 = asyncio.create_task(fetch_data())
    task2 = asyncio.create_task(process_data())
    await task1
    await task2

asyncio.run(main())
  • I/O 멀티플렉싱 기술 기반
  • 준비된 작업을 지속적으로 모니터링하고 스케줄링
  • Linux에서는 epoll, macOS에서는 kqueue 사용

저수준 구현 파헤치기

소켓 서버 with 이벤트 루프

import selectors
import socket

sel = selectors.DefaultSelector()

def accept(sock, mask):
    conn, addr = sock.accept()
    sel.register(conn, selectors.EVENT_READ, read)

def read(conn, mask):
    data = conn.recv(1024)
    response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{"status":"ok"}'
    conn.send(response.encode())
    sel.unregister(conn)
    conn.close()

sock = socket.socket()
sock.bind(('localhost', 8000))
sock.listen()
sel.register(sock, selectors.EVENT_READ, accept)

while True:
    events = sel.select()
    for key, mask in events:
        key.data(key.fileobj, mask)
  • 동시 접속 10,000개 처리 가능 (이론상)
  • 전통적인 동기 방식 대비 100배 이상의 성능 향상

성능 비교 분석

방식 동시 연결 처리 CPU 사용률 메모리 효율성
동기 스레드 △ (제한적) 높음 낮음
이벤트 루프 ◎ (수만 개) 매우 낮음 높음

실전 패턴 3가지

  1. 병렬 태스크 처리:
  2. async def batch_process(items): tasks = [process_item(item) for item in items] return await asyncio.gather(*tasks)
  3. 타임아웃 제어:
  4. try: await asyncio.wait_for(api_call(), timeout=3.0) except asyncio.TimeoutError: logger.error("Request timed out")
  5. 백프레셔 구현:
  6. from fastapi import BackgroundTasks

async def data_pipeline(data):
queue = asyncio.Queue(maxsize=100)
# 생산자-소비자 패턴 구현


---

### **주의해야 할 함정**  
1. **CPU 집약적 작업 블로킹**:  
```python
# 잘못된 예시
@app.get("/calculate")
async def heavy_calculation():
    result = sync_cpu_intensive_task()  # 동기 함수 호출
    return result

# 올바른 해결책
from concurrent.futures import ProcessPoolExecutor

async def wrapper():
    loop = asyncio.get_event_loop()
    with ProcessPoolExecutor() as pool:
        return await loop.run_in_executor(pool, sync_cpu_intensive_task)
  1. 세션 관리:
    # 클라이언트 재사용 필수
    async def get_http_client():
     return httpx.AsyncClient(timeout=10)

결론

FastAPI의 비동기 처리 능력은 코루틴-이벤트 루프-I/O 멀티플렉싱 삼각편대에서 나옵니다.

실제 벤치마크에서 FastAPI는 Node.js 기반 Express와 비교해 2배 이상의 RPS(Requests Per Second)를 기록하기도 하는데요.

단순히 async/await 문법 추가가 아닌, 파이썬 런타임의 깊은 이해가 고성능 백엔드 개발의 관건입니다.