본문 바로가기
프로젝트/트러블슈팅

트러블슈팅 - sqlalchemy.exc.MissingGreenlet

by 데브조이 2025. 4. 11.
반응형

개요

회사에서 담당하고 있는 프로젝트는 FastAPI로 구현되어 있는데, 새로운 기능을 추가하면서 SQLAlchemy를 점진적으로 도입하고 있다.
오늘은 그 과정에서 마주한 MissingGreenlet에 대해 다뤄보려고한다.

회사 코드를 그대로 유출할 수 없기 때문에, 작성한 코드는 다른 예제로 변환하여 작성하였다. 


개발 환경

  • python 3.10
  • fastapi 0.104.1
  • sqlalchemy 2.0.39

상세 에러

[   ERROR] (sqlalchemy.exc.MissingGreenlet) greenlet_spawn has not been called;
can't call await_only() here.
Was IO attempted in an unexpected place?

SQLAlchemy 공식 문서에 따르면 MissingGreenlet 은 비동기 DBAPI 호출이 greenlet 컨텍스트 외부에서 발생했을 때 발생하는 오류이다. 주로 async_session.execute() 같은 SQLAlchemy 비동기 메서드를 잘못된 방식으로 호출할 때 발생한다. 

일반적으로 해당 에러가 발생하는 주원인은 아래와 같다.

  • Lazy Loading (지연 로딩) 사용: ORM의 lazy loading은 기본적으로 asyncio에서 지원되지 않음.
  • await 키워드 없이 비동기 DB 호출 실행. 

상황 및 원인

1. db.py - 데이터베이스 connection  설정을 위한 코드

class DatabaseManager:
	# ... 
    
    def __init__(self, db_name: str):
        self.__engine = create_async_engine(
            f"mysql+aiomysql://...{설정}")
        self.__sessionmaker = sessionmaker(
            bind=self.__engine, class_=AsyncSession, expire_on_commit=False, autoflush=False, autocommit=False)

    @asynccontextmanager
    async def get_session(self):
        async with self.__sessionmaker() as session:
            yield session

	# ...

 

2. services/apple.py - DB 조회를 위한 Repository 레이어와 로직 처리를 위한 Service 레이어 코드

class AppleServiceImpl(AppleService):
    # ...
    async def get_apple(self, apple_idx: int) -> AppleSingleResponse:
        async with self.__db_manager.get_session() as session:
            try:
                apple = await self.__get_verified_apple_by_idx(session=session, apple_idx=apple_idx)
                return AppleSingleResponse.from_(apple=apple)
            except CustomError as e:
                await session.rollback()
                raise e
            except Exception as e:
                await session.rollback()
                raise ServerError.to(
                    code=ResponseCode.CUSTOM_ERROR,
                    message=e
                )
    # ...


class AppleRepositoryImpl(AppleRepository):
    # ...
    async def find_by_idx(self, session: AsyncSession, apple_idx: int) -> Apple | None:
        result = await session.execute(
            select(Apple)
            .where(Apple.idx == apple_idx,
            	# ...
            )
        )
        return result.scalar_one_or_none()
    # ...

 

기존에 aiomysql 드라이버를 이용해 비동기로 작성한 코드를 ORM 방식으로 마이그레이션 하다가 해당 에러를 직면했다.

근본적인 문제는 비동기 환경에서 ORM을 사용하는 것이었다.

Lazy Loading 은 객체 조회 시 즉시 쿼리를 실행하지 않고 필요시 쿼리를 실행한다. 만약 비동기 환경에서 ORM 객체를 조회할 때 Lazy Loading을 사용하면 awit 없이 동기적으로 DB 호출을 수행한다.

이에 따라 MissingGreenlet 이 발생할 수 있다.

AsyncSession는 순수하게 비동기 방식으로 작동하고자 하고, SQLAlechemy는 Lazy Loading 때문에 동기적으로 DB 조회를 수행하니 문제가 발생한 것이다.


해결 방법

이 문제를 해결하기 위한 방법으로 동기 방식 사용, Eager Loading 등이 있을 수 있다.
개인적으로 판단했을 때, 현재 구현하고자 하는 API는 단순 CRUD 작업이기 때문에 비동기를 반드시 사용해야 할 이유가 없다고 생각해서 동기 방식을 가져가기로 했다.

이에 따라 아래와 같이 db.py의 코드를 수정하였고, route와 service, repository의 코드도 동기적으로 동작하도록 수정하였다.

class DatabaseManager:
	# ... 
    
    def __init__(self):
        # 1. create_engine 사용 / pymysql 사용 / AsyncSession 옵션 제거 
        self.__engine = create_engine(
            f"mysql+pymysql://...{설정}")
        self.__sessionmaker = sessionmaker(
            bind=self.__engine, expire_on_commit=False, autoflush=False, autocommit=False)
	
    # 2. @contextmanager 사용 / 동기 메서드 전환 / Session 사용
    @contextmanager
    def get_session(self) -> Session:
        session = self.__sessionmaker()
        try:
            yield session
            session.commit()
        except Exception as e:
            session.rollback()
            raise e
        finally:
            session.close()

	# ...

 


마치며

그저 FastAPI가 비동기 프로그래밍을 크게 지원한다는 강점을 생각하고 무의식적으로 적용해 온 것이 아닌지 생각해 보는 계기가 되었다.
앞으로는 구현하고자 하는 기능에 비동기가 필요한지 생각하고 코드를 작성할 필요가 있을 것 같다.


참고 자료

-SQLAlchemy 공식 문서(MissingGreenlet): https://docs.sqlalchemy.org/en/20/errors.html#error-xd2s

반응형