FastAPI 서버개발, FastAPI의 비동기 요청 처리 이해하기

FastAPI는 빠르고 현대적인 웹 프레임워크로, Python 3.7 이상에서 사용할 수 있습니다. FastAPI는 ASGI를 기반으로 하여 비동기 프로그래밍을 쉽게 활용할 수 있도록 도와줍니다. 이 글에서는 FastAPI를 사용하여 비동기 요청 처리를 구현하는 방법을 상세히 설명하고, 예제 코드를 통해 이해를 돕겠습니다.

1. FastAPI와 비동기 프로그래밍의 이해

비동기 프로그래밍은 주로 I/O 작업이 많은 작업에서 성능을 크게 개선할 수 있는 기술입니다. 동기식 프로그래밍에서는 요청이 완료될 때까지 해당 요청을 처리하는 스레드가 대기하게 됩니다. 하지만 비동기 프로그래밍에서는 자원이 낭비되지 않도록 다른 작업을 동시에 수행할 수 있습니다.

1.1 FastAPI의 비동기 지원

FastAPI는 비동기 함수(`async def`)를 사용하여 I/O 바운드 작업을 처리할 수 있게 지원합니다. FastAPI의 경로 작업(route handler)에서 비동기 함수를 사용하면, 비동기적으로 요청을 처리할 수 있어 다른 요청이 대기하지 않고 곧바로 처리됩니다.

1.2 비동기와 동기 함수 비교

FastAPI에서 비동기 경로 작업을 구현하는 방법은 동기 함수와 유사하지만, 반드시 `async` 키워드를 사용하여 정의해야 합니다. 아래는 비동기와 동기 함수의 예제 코드입니다.

from fastapi import FastAPI
import time

app = FastAPI()

# 동기 경로 작업
@app.get("/sync")
def read_sync():
    time.sleep(2)  # 2초 대기
    return {"message": "동기 요청 처리 완료"}

# 비동기 경로 작업
@app.get("/async")
async def read_async():
    await asyncio.sleep(2)  # 2초 대기
    return {"message": "비동기 요청 처리 완료"}

위의 코드는 `/sync` 경로를 통해 동기적으로 요청을 처리하고, `/async` 경로를 통해 비동기적으로 요청을 처리합니다. 요청이 들어온 순서에 따라 동기 요청은 처리하는 동안 다른 요청이 대기하게 되지만, 비동기 요청은 대기하는 동안 다른 요청을 처리할 수 있습니다.

2. FastAPI에서 비동기 I/O 작업 처리하기

FastAPI에서 비동기 I/O 작업은 데이터베이스의 CRUD 작업, 웹 API 호출, 파일 업로드 등 다양한 경우에 활용됩니다. 각 I/O 작업이 비동기적으로 수행되면, 애플리케이션의 성능을 높일 수 있습니다.

2.1 비동기 데이터베이스 작업

다양한 비동기 데이터베이스 드라이버가 FastAPI와 함께 사용될 수 있습니다. 예를 들어, databases라는 비동기 ORM 라이브러리와 함께 사용하여 비동기적으로 데이터베이스와 상호작용할 수 있습니다.

import asyncio
from fastapi import FastAPI
import databases

DATABASE_URL = "sqlite:///./test.db"
database = databases.Database(DATABASE_URL)

app = FastAPI()

@app.on_event("startup")
async def startup():
    await database.connect()

@app.on_event("shutdown")
async def shutdown():
    await database.disconnect()

@app.get("/items/")
async def read_items():
    query = "SELECT * FROM items"
    return await database.fetch_all(query)

위의 코드는 `databases` 라이브러리를 사용하여 SQLite 데이터베이스에 비동기적으로 연결하고, 아이템을 읽어오는 예시입니다. FastAPI의 생명주기 이벤트를 통해 서버가 시작할 때와 종료할 때 데이터베이스 연결을 관리할 수 있습니다.

2.2 비동기 HTTP 요청 처리

FastAPI에서는 httpx 같은 라이브러리를 사용하여 비동기 HTTP 요청도 쉽게 처리할 수 있습니다. 비동기 HTTP 요청은 외부 API와의 통신이 필요할 때 매우 유용합니다.

import httpx

@app.get("/external/")
async def read_external():
    async with httpx.AsyncClient() as client:
        response = await client.get('https://api.example.com/data')
    return response.json()

위의 코드는 외부 API에서 데이터를 가져와서 반환하는 비동기 경로 작업입니다. 비동기 클라이언트를 사용함으로써, 데이터 요청 도중 서버가 다른 요청을 처리할 수 있도록 합니다.

3. 비동기 작업에서의 오류 처리

비동기 작업을 구현할 때, 오류 처리 또한 중요합니다. 비동기 작업이 실패할 경우, 적절한 오류 메시지를 반환해야 하며, 이때 HTTPException을 사용할 수 있습니다.

from fastapi import HTTPException

@app.get("/data/")
async def read_data(param: str):
    if param not in ["valid1", "valid2"]:
        raise HTTPException(status_code=404, detail="데이터를 찾을 수 없습니다.")
    return {"message": f"{param}에 대한 데이터"}

위의 코드는 파라미터가 유효하지 않을 때 HTTP 404 오류를 반환하는 예제입니다. FastAPI의 예외 처리 메커니즘은 비동기 방식에서도 동일하게 작동합니다.

4. FastAPI의 비동기 요청 처리 성능 비교

비동기 요청 처리와 동기 요청 처리의 성능 차이를 비교할 수 있는 방법도 중요합니다. 아래는 FastAPI의 비동기 및 동기 경로 작업에 대한 성능 테스트 예제입니다.

import time
import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/sync-test/")
def sync_test():
    time.sleep(1)  # 1초 대기
    return {"message": "비동기 요청이 아닙니다."}

@app.get("/async-test/")
async def async_test():
    await asyncio.sleep(1)  # 1초 대기
    return {"message": "비동기 요청입니다."}

이제 이 두 경로를 동시에 호출하여 성능을 비교할 수 있습니다. 테스트 도구(예: Apache Bench 또는 wrk)를 사용하여 두 경로에서의 응답 시간을 측정해보세요.

5. 결론

FastAPI는 비동기 요청 처리를 지원하여 높은 성능을 기록할 수 있는 프레임워크입니다. I/O 작업을 비동기적으로 처리함으로써, 서버의 자원을 효율적으로 활용할 수 있습니다. 이번 글에서는 FastAPI의 비동기 요청 처리 방식에 대해 설명하고 여러 예제를 통해 이해를 도왔습니다.

FastAPI를 사용하면 비동기적 아키텍처를 쉽게 구현할 수 있으며, 이에 따라 생산성과 성능이 향상됩니다. 비동기 함수를 적절히 활용하여 비즈니스 로직을 구현하고, 다양한 응용 프로그램을 개발해보세요!