FastAPI에서 비동기 DB 커넥션 풀 관리하기

문제 상황

기존 Django 기반 API를 FastAPI로 전환하는 프로젝트를 진행 중이다. 동기 방식의 DB 쿼리를 비동기로 변경하면서 커넥션 풀 관리가 예상보다 까다로웠다.

초기에는 각 요청마다 DB 커넥션을 생성했더니 부하 테스트에서 커넥션 고갈 문제가 발생했다.

해결 방법

SQLAlchemy 1.4의 async 엔진을 사용해 커넥션 풀을 구성했다.

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

engine = create_async_engine(
    "postgresql+asyncpg://user:pass@localhost/db",
    pool_size=20,
    max_overflow=10,
    pool_pre_ping=True
)

AsyncSessionLocal = sessionmaker(
    engine, class_=AsyncSession, expire_on_commit=False
)

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

FastAPI의 Dependency Injection을 활용해 각 엔드포인트에서 세션을 주입받도록 구성했다.

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User).filter(User.id == user_id))
    user = result.scalar_one_or_none()
    return user

주의사항

pool_pre_ping=True 설정이 중요했다. DB 재시작 시 stale 커넥션을 자동으로 감지해 재연결한다.

contextmanager를 사용할 때 expire_on_commit=False 옵션을 주지 않으면 커밋 후 객체 속성 접근 시 추가 쿼리가 발생하는 문제가 있었다.

기존 Django ORM에 익숙한 팀원들을 위해 간단한 마이그레이션 가이드도 작성했다. asyncpg 드라이버가 psycopg2보다 성능이 좋다는 벤치마크 결과도 함께 공유했다.

FastAPI에서 비동기 DB 커넥션 풀 관리하기