[FastAPI]Migrating from Async to Sync

ww_ung·2025년 4월 12일

SKALA

목록 보기
21/25

FastAPI는 async/await 기반의 비동기 처리 덕분에 고성능 API 서버 구축에 최적화되어 있다. 그렇다면 반대로, 동일한 기능을 동기 방식으로 구현한다면 어떤 차이가 생길지 알아보자.

이 글에서는 FastAPI 비동기 코드를 동기 방식으로 전환하여 직접 비교해보고, 그 과정을 통해 비동기 방식의 이점이 단순한 속도 이상의 의미임을 검증해보고자 한다.

동기 방식으로 전환하며 바뀌는 핵심 요소

FastAPI의 비동기 구조를 동기 방식으로 전환하기 위해서는 몇 가지 명확한 수정이 필요하다. 아래는 개발하며 주로 변경되는 핵심 요소들이다.

DB 드라이버의 변경

# 동기
ASYNC_DB_URL = "mysql+aiomysql://user:pass@localhost:3306/dbname"
# 비동기
DB_URL = "mysql+pymysql://user:pass@localhost:3306/dbname"

aiomysql은 비동기 전용 드라이버이고, pymysql은 동기 전용이다.

ORM(SessionLocal, SQLAlchemy 등)이 에러 없이 작동하려면 DB 드라이버와 ORM 방식이 일치해야 한다.

라우터 함수 선언의 차이

# 동기
@router.post("/tasks")
def create_task(...):
    ...

# 비동기
@router.post("/tasks")
async def create_task(...):
    ...

async def 대신 일반적인 def로 선언

FastAPI는 자동으로 동기/비동기 방식을 구분하여 처리하므로, 내부 코드 실행 방식에 맞춰 명확하게 선언해야 한다.

DB 세션 주입 함수의 차이

# 동기
def get_db() -> Session:
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
# 비동기
async def get_db() -> AsyncSession:
    async with AsyncSessionLocal() as session:
        yield session

비동기에서는 async with, 동기에서는 try-finally를 사용해 세션을 관리한다.

비동기 컨텍스트 매니저를 통해 처리한다.

ORM DBMS 함수 호출

# 동기 방식
return task_crud.create_task(db, task_body)

# 비동기 방식
return await task_crud.create_task(db, task_body)

await가 없으면 비동기 함수는 단순히 coroutine 객체만 반환하고, 실행되지 않는다.

동기 방식에서는 await 없이 바로 리턴한다.

Trouble Shooting

⚠️ FastAPI 다중 진입점 충돌 문제 해결

python3 -m uvicorn api.main2:app --host 0.0.0.0 --port 8004 --reload

위 명령어로 main2.py를 실행하려 했지만 실제로는 main.py의 app 객체가 실행

--reload 옵션은 uvicorn이 파일 변경을 감지해 재시작하는 데몬을 띄웁니다. 이 때 최초 실행 지점의 모듈 경로 또는 import 로직이 꼬일 수 있다.

✅ main.py 와 main2.py에 app=FastAPI()가 동일하게 있게되므로 이전 main 삭제 조치 후 해결

⚠️ FastAPI 오류 방지 get_task() 사전조회의 필요성

router의 API에서 update_task() 혹은 delete_task()함수만 호출했더니
ORM(SQLAlchemy)오류 발생

original에서 실제 DB객체가 없으므로 NoneType 에러 유발

500 Internal Server Error
AttributeError: 'NoneType' object has no attribute 'title'

✅ 해결

@router.put("/tasks/{task_id}", response_model=TaskCreateResponse)
def update_task(task_id: int, task_body: TaskCreate, db: Session = Depends(get_db)):
    db_task = task_crud.get_task(db, task_id)
    if db_task is None:
        raise HTTPException(status_code=404, detail="Task not found")
    return task_crud.update_task(db=db, task_create=task_body, original=db_task)

먼저 get_task()로 DB에서 해당 task가 존재하는지 확인 후 전달

🤔 회고

동기 방식은 구현이 단순하고, 구조가 직관적이라는 장점이 있다. 특히 대부분의 CRUD 중심 API 서버에서는 동기 처리만으로도 충분히 빠른 응답 속도를 제공할 수 있음을 확인했다. 또한, 요청 흐름이 순차적으로 처리되기 때문에 예외 상황에 대한 디버깅이 비교적 용이하다는 점도 큰 장점으로 느껴졌다.

반면, 비동기 방식은 수천 개 이상의 요청을 병렬로 처리해야 하는 환경에서 매우 유리하다. 외부 API 호출이나 데이터베이스 접근처럼 I/O 작업이 많은 경우에도 응답이 블로킹되지 않고, 전체 시스템의 처리 효율을 향상시킬 수 있었다. 특히 실시간성과 응답 속도가 중요한 서비스에서는 비동기 구조가 필수적이라는 점을 체감할 수 있었다.

ORM은 단순히 SQL을 대체하는 수준을 넘어, Python 객체를 통해 테이블과 레코드를 표현할 수 있는 강력한 도구로 활용하였다. 이를 통해 쿼리 작성이 훨씬 안전하고 명확해졌으며, 복잡한 조인이나 조건문도 Python 문법으로 처리할 수 있어 생산성과 유지보수성이 크게 향상되었다.

0개의 댓글