FastAPI 비동기 처리 중 DB 커넥션 풀 고갈 이슈

문제 상황

Flask 기반 API 서버를 FastAPI로 마이그레이션한 후, 동시 요청이 50개를 넘어가면 TimeoutError: QueuePool limit exceeded가 발생했다. 기존 Flask에서는 문제가 없었던 부분이라 당황스러웠다.

원인 분석

FastAPI는 기본적으로 비동기 처리를 하지만, SQLAlchemy 1.4의 동기 방식 엔진을 그대로 사용하고 있었다. 각 요청마다 커넥션을 획득하지만 await가 아닌 블로킹 방식으로 동작해서, 실제로는 커넥션을 오래 점유하는 상황이었다.

# 문제가 있던 코드
engine = create_engine(
    DATABASE_URL,
    pool_size=10,
    max_overflow=20
)

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    with SessionLocal() as session:
        user = session.query(User).filter(User.id == user_id).first()
        return user

해결 방법

SQLAlchemy 1.4의 async 엔진으로 전환하고, 세션 관리를 의존성 주입 패턴으로 변경했다.

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

engine = create_async_engine(
    DATABASE_URL.replace("postgresql://", "postgresql+asyncpg://"),
    pool_size=20,
    max_overflow=40,
    pool_pre_ping=True
)

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

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

결과

동시 요청 200개까지 안정적으로 처리되는 것을 확인했다. 응답 시간도 평균 120ms에서 80ms로 개선됐다. asyncpg 드라이버의 성능이 psycopg2보다 확실히 좋다는 것도 체감할 수 있었다.

비동기 프레임워크를 쓴다면 DB 레이어까지 완전히 비동기로 가는 것이 맞다는 교훈을 얻었다.