
SQLAlchemy MissingGreenlet은 FastAPI에서 async SQLAlchemy를 쓸 때 자주 만나는 오류입니다. 대개 비동기 DB IO가 허용되지 않는 시점에 lazy loading이나 만료된 속성 접근이 일어나면서 발생합니다.
핵심은 DB 접근은 await 가능한 구간에서 끝내고, 응답 직렬화 단계에서 새 IO가 일어나지 않게 만드는 것입니다.

SQLAlchemy MissingGreenlet이 말하는 것
SQLAlchemy 공식 오류 문서는 MissingGreenlet을 greenlet_spawn이 설정되지 않은 곳에서 await_only가 호출되는 상황으로 설명합니다. 쉽게 말하면, async DB 작업이 필요한데 지금 위치가 그 작업을 기다릴 수 있는 흐름이 아니라는 뜻입니다.
FastAPI에서는 엔드포인트 안에서 await session.execute(…)를 잘 썼는데도, return 이후 response_model 직렬화 중 ORM 속성을 읽다가 문제가 생기는 경우가 많습니다.
가장 흔한 흐름: lazy loading
ORM relationship이 lazy loading 상태라면, 속성에 접근하는 순간 추가 SQL이 실행될 수 있습니다. 동기 SQLAlchemy에서는 이런 동작이 자연스러워 보일 수 있지만, async 환경에서는 그 시점이 await 가능한지 중요합니다.
@app.get("/users/{user_id}", response_model=UserOut)
async def get_user(user_id: int, session: AsyncSession = Depends(get_session)):
user = await session.get(User, user_id)
return user # response_model이 user.posts를 읽는 순간 lazy load가 발생할 수 있다엔드포인트 함수 안에서는 조회가 끝난 것처럼 보입니다. 하지만 response_model이 중첩 relationship을 읽으면 새로운 DB IO가 뒤늦게 발생할 수 있습니다.
해결 방향 1: 필요한 관계를 미리 로드한다
응답에 posts가 필요하다면, return 이후가 아니라 쿼리 단계에서 미리 로드해야 합니다. selectinload 같은 eager loading 전략을 쓰면 응답 직렬화 전에 필요한 데이터를 준비할 수 있습니다.
from sqlalchemy import select
from sqlalchemy.orm import selectinload
stmt = (
select(User)
.options(selectinload(User.posts))
.where(User.id == user_id)
)
result = await session.execute(stmt)
user = result.scalar_one()
return user해결 방향 2: ORM 객체를 응답 DTO로 변환한다
ORM 객체를 그대로 반환하면 직렬화 시점에 어떤 속성을 읽을지 추적하기 어렵습니다. 필요한 필드만 명시적으로 읽어 dict나 Pydantic 모델로 변환하면, DB 접근 시점을 엔드포인트 안으로 끌어올 수 있습니다.
return UserOut(
id=user.id,
name=user.name,
posts=[PostOut(id=post.id, title=post.title) for post in user.posts],
)이 방식은 코드가 조금 길어지지만, API 응답 계약과 ORM 로딩 전략을 분리할 수 있습니다.
expire_on_commit도 확인해야 한다
SQLAlchemy 세션은 commit 이후 객체 속성을 만료시키는 동작과 연결될 수 있습니다. async 환경에서 commit 뒤 만료된 속성을 다시 읽으면 추가 IO가 필요해질 수 있습니다. SQLAlchemy asyncio 문서에서는 AsyncSession 설정과 implicit IO 방지 방법을 함께 다룹니다.
async_session = async_sessionmaker(
engine,
expire_on_commit=False,
)이 설정이 항상 정답이라는 뜻은 아닙니다. 다만 commit 이후 응답 객체를 만들 때 만료된 속성을 다시 읽는 흐름이 있는지 확인해야 합니다.
ResponseValidationError와 함께 보이는 이유
FastAPI는 response_model에 맞춰 반환값을 직렬화합니다. 이 과정에서 ORM 객체의 속성을 읽습니다. 만약 그 속성이 lazy relationship이거나 만료된 attribute라면, 직렬화 중 DB IO가 발생할 수 있습니다.
그래서 MissingGreenlet과 ResponseValidationError가 서로 완전히 별개처럼 보이지만, 실제로는 response serialization이라는 같은 구간에서 이어질 수 있습니다.
디버깅 순서
- 오류 stack trace에서 어떤 속성을 읽다가 터졌는지 찾는다
- 그 속성이 lazy relationship인지 확인한다
- return 전에 필요한 데이터를 eager load한다
- commit 이후 속성 만료가 있는지 본다
- ORM 객체 대신 응답 DTO로 변환해 재현 여부를 확인한다
피해야 할 임시 처방
- lazy=’joined’를 모든 관계에 무조건 붙이기
- response_model을 제거해 오류만 숨기기
- 세션 생명주기를 너무 길게 늘려 문제를 덮기
- async 엔드포인트 안에서 동기 세션을 섞어 쓰기
정리
SQLAlchemy MissingGreenlet은 FastAPI가 문제라기보다, async ORM에서 암묵적 IO가 발생하는 시점이 잘못됐다는 신호입니다.
필요한 데이터는 await 가능한 쿼리 구간에서 명시적으로 로드하고, 응답 직렬화 단계에서는 새 DB 접근이 일어나지 않게 해야 합니다. FastAPI의 응답 계약 문제는 FastAPI ResponseValidationError 글과 함께 보면 흐름이 더 잘 보입니다. 공식 설명은 SQLAlchemy MissingGreenlet 문서와 SQLAlchemy asyncio 문서를 기준으로 확인할 수 있습니다.