개념정리/개발

FastAPI와 비동기 처리

backtwobasic 2025. 2. 9. 16:00

FastAPI

FastAPI는 비동기 처리와 고성능을 위한 강력한 기능들을 제공하는 웹 프레임워크로, 간단한 특징 및 비동기 처리 과정에 대해 설명하겠다. 

 

FastAPI 개요 및 특징

FastAPI는 Python 3.6 이상의 버전에서 사용 가능한 웹 프레임워크로, 아래와 같은 특징이 있다.

  1. 고속: FastAPI는 Starlette을 기반으로 하여 매우 빠른 속도를 자랑함
  2. 비동기 처리 지원: FastAPI는 비동기 API 처리를 기본적으로 지원하며, I/O 작업에서 성능이 크게 개선됨
  3. 자동 문서화: Swagger와 ReDoc을 통해 API 문서화가 자동으로 이루어지며, 개발과 테스트의 용이
  4. 데이터 검증 및 schema 관리: Pydantic을 이용하여 데이터 검증에 용이하고, schema 관리에 용이

 

1. FastAPI 성능 최적화: FastAPI에서의 비동기 처리

FastAPI는 비동기(Asynchronous) 처리를 기본적으로 지원한다. 이는 I/O 작업(예: DB 쿼리, 외부 API 호출)에서 성능을 획기적으로 개선할 수 있으며 주요 특징이라고 볼 수 있다. 

예) 비동기 API 처리

from fastapi import FastAPI 
import asyncio 

app = FastAPI() 

@app.get("/items/{item_id}") 
async def read_item(item_id: int): 
	await asyncio.sleep(1) # 비동기 대기 
    return {"item_id": item_id}

 

  •  비동기 함수: async def로 선언한 함수는 비동기적으로 실행되어 다른 요청들을 기다릴 필요 없이 처리할 수 있음
  • await: I/O 작업을 비동기적으로 처리할 때 await를 사용

비동기 I/O 작업을 통한 성능 최적화

FastAPI의 가장 큰 장점 중 하나는 비동기 I/O 작업 처리다. FastAPI는 비동기 처리를 위한 ASGI(Asynchronous Server Gateway Interface)를 기본으로 제공하며, 비동기 처리는 I/O 바운드 작업에서 성능을 크게 개선할 수 있기 때문에, 대규모 웹 애플리케이션에 필수적인 기술 I/O가 많이 필요한 작업을 비동기적으로 처리하면, 서버가 다른 요청을 블로킹 없이 처리할 수 있다. 예를 들어, DB 조회나 외부 API 호출을 비동기 방식으로 처리할 수 있다.

예시: 비동기 DB 쿼리

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine 
from sqlalchemy.orm import sessionmaker 

DATABASE_URL = "postgresql+asyncpg://user:password@localhost/testdb" 
engine = create_async_engine(DATABASE_URL, echo=True) 
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) 

async def get_user(db: AsyncSession, user_id: int): 
	result = await db.execute(select(User).filter(User.id == user_id)) 
    return result.scalars().first()

 

FastAPI에서 비동기 I/O 작업을 최적화하려면, 비동기 방식으로 작동하는 라이브러리들을 적극적으로 활용하는 게 중요하다. 예를 들어:

  • 비동기 SQLAlchemy : 비동기 DB 쿼리 방식으로 I/O 차단 없이 여러 요청을 동시에 처리할 수 있음
  • 비동기 DB 라이브러리: SQLAlchemy의 asyncpg를 이용한 비동기 DB 쿼리, databases 라이브러리를 이용한 비동기 DB 처리
  • 비동기 HTTP 클라이언트: httpx는 비동기 HTTP 요청을 보내는 데 사용. 이를 활용하여 외부 API 호출을 비동기적으로 처리

비동기 함수에서의 asyncio 활용

비동기 함수에서는 I/O 차단 없이 여러 작업을 동시에 처리할 수 있기 때문에, 효율적으로 여러 작업을 병렬 처리하는 것이 가능하다. 예를 들어, 여러 DB 조회나 외부 API 호출을 동시에 처리할 때 asyncio.gather()를 사용할 수 있으며, 이는 많이 사용된다. 

import asyncio 
import httpx 

async def fetch_data_from_api(api_url: str): 
	async with httpx.AsyncClient() as client: 
    response = await client.get(api_url) 
    return response.json() 
    
async def fetch_multiple_data():
	api_urls = [ 
    	"https://api.example1.com", 
        "https://api.example2.com", 
        "https://api.example3.com", 
    ] 
    
    tasks = [ fetch_data_from_api(url) for url in api_urls ] 
    results = await asyncio.gather(*tasks) 
    
    return results

 

위 예시 코드에서 asyncio.gather()는 여러 비동기 작업을 병렬로 처리할 수 있기 때문에 응답 시간을 크게 줄여줄 수 있다. 

 

 

2. FastAPI 성능 최적화: Uvicorn + Gunicorn 활용

FastAPI는 Uvicorn을 ASGI 서버로 사용하며, Gunicorn을 통해 여러 작업자가 동시에 요청을 처리할 수 있다. 이를 통해 멀티코어 CPU를 활용하여 성능을 극대화할 수 있다.

Gunicorn + Uvicorn 설정

pip install gunicorn uvicorn

gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app
# 또는
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 --loop asyncio --http h11
  • -w 4: 워커 프로세스 수를 지정. (여기서는 4개)
  • -k uvicorn.workers.UvicornWorker: Gunicorn에 Uvicorn을 워커로 사용
  • --workers: 여러 워커를 사용하여 멀티코어 CPU 활용
  • --host 0.0.0.0: 모든 IP에서 접근 가능하게 설정
  • --port: 포트 번호 지정 (기본값 8000)
  • --http h11: HTTP/1.1을 지원하며, 성능을 더 높일 수 있음

Uvicorn + Gunicorn 멀티 워커 설정

Uvicorn은 비동기 처리를 잘 지원하지만, 멀티코어 서버에서 성능을 극대화하려면 Gunicorn Uvicorn Worker를 사용해 여러 개의 워커를 실행하는 것이 필요하다. 

gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app
  • -w 4: 4개의 워커 프로세스를 사용하여 CPU 코어를 최대한 활용
  • -k uvicorn.workers.UvicornWorker: Uvicorn을 워커로 사용하여 비동기 처리가 가능하게 함

 

3. FastAPI 성능 최적화: 캐싱

FastAPI에서 API 성능을 최적화하려면 캐싱을 활용하는 것이 중요하다. Redis와 같은 인메모리 데이터베이스를 이용해서 자주 조회되는 데이터를 캐시하면, 서버의 부하를 줄이고 반복되는 요청에 대한 응답 속도를 크게 개선해 응답 속도를 대폭 개선할 수 있다.

Redis 캐싱 전략

FastAPI와 Redis를 결합하여 자주 요청되는 데이터를 캐시하고, 빠르게 조회할 수 있도록 할 수 있다. 

  • 캐시 만료 시간을 설정하여 오래된 데이터를 자동으로 갱신하는 방식도 구현할 수 있음
  • 캐시 일관성 유지: 데이터가 변경될 때마다 캐시를 갱신하거나 무효화
  • 캐시 만료 시간(setex)을 설정하여 주기적으로 캐시를 갱신
예시: Redis 캐싱 사용

import redis 
from fastapi import FastAPI 

app = FastAPI() 
cache = redis.Redis(host="localhost", port=6379, db=0) 

@app.get("/items/{item_id}") 
async def get_item(item_id: int): 
	cached_item = cache.get(f"item:{item_id}") 
    if cached_item: 
    	return {"item_id": item_id, "cached": True} 
        # 데이터베이스에서 아이템을 가져오는 로직 (생략) 
        # item = {"item_id": item_id, "name": "Example"} 
        # cache.set(f"item:{item_id}", str(item)) 
        # return item
        
  
예시2 : Redis 캐싱 사용2

import redis 
from fastapi import FastAPI 

cache = redis.Redis(host='localhost', port=6379, db=0) 

@app.get("/items/{item_id}") 
async def get_item(item_id: int): 
	cached_item = cache.get(f"item:{item_id}") 
    if cached_item: 
    	return {"item_id": item_id, "cached": True, "data": cached_item.decode()} 
    item = fetch_from_db(item_id) # DB에서 아이템을 가져오는 로직 
    cache.setex(f"item:{item_id}", 60, item) # 캐시 저장 (60초 동안 유효) 
    return item

데이터 페이징

많은 데이터를 한 번에 반환하는 것보다는 페이징 처리를 통해 작은 단위로 데이터를 반환하는 것이 성능 면에서 훨씬 효율적이다. 대량의 데이터를 한 번에 반환하지 않고, 페이징 처리를 통해 성능을 높일 수 있으며, 이는 클라이언트가 요청할 때, 원하는 범위의 데이터만 반환하는 방식이다.

@app.get("/items/") 
async def get_items(skip: int = 0, limit: int = 10):
	items = db.query(Item).offset(skip).limit(limit).all() 
    return items
  • 페이징: 데이터의 양이 많을 때, 한 번에 데이터를 모두 조회하지 않고작은 단위로 나눠서처리하면 성능이 크게 향상
  • skip: 시작 인덱스.
  • limit: 반환할 데이터의 개수.

페이징 처리는 성능을 크게 향상시킬 수 있어, 특히 데이터베이스에서 큰 테이블을 조회할 때 유용하다. 

 

 

4. FastAPI 성능 최적화: HTTP/2 및 WebSocket 활용

HTTP/2는 병렬 요청 헤더 압축을 지원하여, 더 빠른 통신을 가능하게 한다. FastAPI는 Uvicorn을 통해 HTTP/2를 지원하며, 이를 통해 고속 통신을 구현할 수 있다. 

HTTP/2 설정

uvicorn main:app --http h2 --workers 4
  • HTTP/2를 사용하면 한 번의 연결로 여러 요청을 병렬로 처리할 수 있다. 대규모 웹 애플리케이션에서 효율적인 리소스 사용이 가능하다. 

WebSocket 활용

WebSocket은 실시간 데이터 전송이 필요한 애플리케이션에서 유용하다. 예를 들어, 실시간 채팅 시스템이나 알림 시스템에서 사용된다.

from fastapi import WebSocket 

@app.websocket("/ws") 
async def websocket_endpoint(websocket: WebSocket):
	await websocket.accept() 
    while True:
    	data = await websocket.receive_text() 
        await websocket.send_text(f"Message text was: {data}")
 

WebSocket을 사용하면 클라이언트와 서버 간에 지속적인 연결을 유지하면서 양방향 통신을 할 수 있다. 

'개념정리 > 개발' 카테고리의 다른 글

boto3  (0) 2025.05.03
Celery와 Redis  (0) 2025.02.09
MSA, Landing Zone  (0) 2025.01.31
Bastion Host  (1) 2025.01.27