Python FastAPI에서 동기 DB 라이브러리 async로 전환하기
문제 상황
FastAPI로 구축한 API 서버의 응답 시간이 점점 느려졌다. 모니터링 결과 동시 요청이 10개만 넘어가도 응답 시간이 급격히 증가했다.
원인은 psycopg2를 사용한 동기 DB 호출이었다. FastAPI는 ASGI 기반이라 async/await를 지원하지만, 동기 라이브러리를 사용하면 이벤트 루프가 블로킹된다.
해결 과정
asyncpg로 전환하기로 결정했다. SQLAlchemy 1.4의 async 지원도 고려했지만, 쿼리가 복잡하지 않아 asyncpg를 직접 사용했다.
기존 코드
import psycopg2
from fastapi import FastAPI
app = FastAPI()
def get_user(user_id: int):
conn = psycopg2.connect(DATABASE_URL)
cur = conn.cursor()
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
result = cur.fetchone()
cur.close()
conn.close()
return result
@app.get("/users/{user_id}")
def read_user(user_id: int):
return get_user(user_id)
변경 후
import asyncpg
from fastapi import FastAPI
app = FastAPI()
pool = None
@app.on_event("startup")
async def startup():
global pool
pool = await asyncpg.create_pool(DATABASE_URL, min_size=10, max_size=20)
@app.on_event("shutdown")
async def shutdown():
await pool.close()
async def get_user(user_id: int):
async with pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT * FROM users WHERE id = $1", user_id
)
return dict(row) if row else None
@app.get("/users/{user_id}")
async def read_user(user_id: int):
return await get_user(user_id)
결과
- 평균 응답 시간: 200ms → 45ms
- 동시 요청 100개 처리 시간: 8초 → 1.2초
- CPU 사용률 30% 감소
커넥션 풀을 적절히 설정하는 것도 중요했다. 초기에 max_size를 50으로 설정했다가 DB 커넥션 한계에 도달해서 20으로 조정했다.
주의사항
asyncpg는 psycopg2와 placeholder 문법이 다르다. %s 대신 $1, $2를 사용한다. 마이그레이션 시 모든 쿼리를 수정해야 했다.
또한 transaction 처리 방식도 달라서 기존 코드를 꼼꼼히 확인해야 했다.