FastAPI에서 SQLAlchemy async 세션 관리 개선
배경
회사 프로젝트에서 FastAPI를 사용하는데, SQLAlchemy를 동기 방식으로 사용하다 보니 비동기 엔드포인트에서 블로킹이 발생했다. SQLAlchemy 1.4가 asyncio를 정식 지원하면서 마이그레이션을 진행했다.
기존 방식의 문제
# 동기 방식
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
return db.query(User).filter(User.id == user_id).first()
비동기 엔드포인트에서 동기 DB 호출이 이벤트 루프를 블로킹했다. run_in_executor로 우회할 수도 있지만 근본적인 해결은 아니었다.
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",
echo=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))
return result.scalars().first()
주의사항
- 드라이버 변경:
psycopg2대신asyncpg사용 - 쿼리 방식:
.query()대신select()사용 - 결과 처리:
.execute()후.scalars()또는.fetchall()필요
성능 개선
부하 테스트 결과 동시 요청 100개 기준으로 응답 시간이 평균 40% 감소했다. DB 연결 대기 시간이 사라지면서 전체 처리량도 개선됐다.
마이그레이션 팁
- 모델 정의는 그대로 유지 가능
- relationship lazy loading은
selectinload명시 필요 - 트랜잭션 처리는
async with session.begin()사용
기존 코드베이스가 크지 않아서 하루 정도 작업으로 마이그레이션을 완료했다. SQLAlchemy 1.4의 타입 힌팅도 개선되어 코드 안정성도 함께 올라갔다.