fastapi-users는 기본 매뉴얼이 비동기로 되어있어 비동기로 보통 작성하곤 한다.
그러면 sqlalchemy의 MissingGreenlet 오류를 자주 만날 수 있게 된다.
MissingGreenlet 오류는 sqlalchemy에서 아래와 같이 설명된다.
A call to the async
DBAPI
was initiated outside the greenlet spawn context usually setup by the SQLAlchemy AsyncIO proxy classes. Usually this error happens when an IO was attempted in an unexpected place, using a calling pattern that does not directly provide for use of the await keyword. When using the ORM this is nearly always due to the use of
lazy loading
, which is not directly supported under asyncio without additional steps and/or alternate loader patterns in order to use successfully.
간단히 말해서 비동기 dialect(alomysql 같은것)을 사용하면 내부적으로 greenlet을 사용하게 되는데, greenlet에서는 IO처리를 항상 비동기로 호출하게끔 강제하고 동기로 호출하면 오류를 내보내는데 이것이 “MissingGreenlet”이다.
그래서 이 오류를 피하고자 하면 lazy loading과 같은 Implicit IO을 막으면 된다.
하지만 이번에 다룰 오류는 lazy loading 때문에 생기는 문제가 아니다.
expire과 관련된 문제인데, 혹시라도 이걸 보고 도움이 되었으면 좋겠다.
## 앞서 OAuth2가 데이터베이스에 저장되는 과정
OAuth2를 통해서 최초 인증을 하게되면 유저 레코드를 삽입하고, OAuthAccount 레코드를 삽입한다.
유저 테이블은 이메일 / 비밀번호 / 유저 상태를 나타내는 유저의 정보가 저장되고,
OAuthAccount 테이블은 리프레시 토큰 / 엑세스 토큰 / 만기일이 같은 정보가 들어간다.
하나의 유저는 여러 OAuthAccount에 대해 1:N 관계로 매핑되며 이를 통해 다양한 소셜 계정으로 인증할 수 있다.
## 문제의 발단
이번에 경험한 오류는 아래 코드에서 발생했다. (fastapi_users의 manager.py파일에서)
# 대충 유저 생성하는 코드
user = await self.user_db.add_oauth_account(user, oauth_account_dict)
for existing_oauth_account in user.oauth_accounts: # user.oauth_accounts에서 오류!
…
앞에서 user 객체를 생성하고 커밋한 뒤 user.oauth_accounts에 OAuthAccount를 생성하고 커밋했다.
근데, 바로 아래 줄의 user.oauth_accounts에 다시 접근하면 OAuthAccount를 새로 로딩해야 한다고 오류를 내뿜는다!
## 문제의 원인과 해결법
이는 user 객체가 expire 되었기 때문이다.
세션은 ORM객체를 identity map으로 관리하고 있는데 트랜잭션이 종료되면 이후 다른 트랜잭션에 의한 데이터 변경에 동기화를 위해 일부러 객체를 expire 한다.
expire하게 되면 객체의 속성들을 삭제하고 다시 접근할때 lazy loading같은 방법으로 데이터베이스에서 불러오게 한다.
이 부분이 문제가 되는 것이다. 동기식 lazy laoding은 Greenlet가 싫어하기 때문이다.
그래서 sqlalchemy로 비동기식 프로그래밍을 할때는 세션에 expire_on_commit을 False로 할것을 권장한다.
https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#asyncio-orm-avoid-lazyloads
물론, 이렇게 하면 다른 트랜잭션에 의해 생기는 변경사항을 불러오지 못할 수 있는데
필요하다면 명시적으로 await session.refresh(obj)를 해줘야 한다.
'프로그래밍 > 스팀 게임 퀴즈' 카테고리의 다른 글
#18 AWS 람다 - 스크래핑한 데이터를 DB에 저장하기 (1) | 2024.01.20 |
---|---|
#17 fastapi-users를 써서 OAuth2를 적용해보았다! (0) | 2024.01.18 |
# 2 프로젝트 설계에 대한 고민 (0) | 2024.01.15 |
#14 프론트 엔드 구축 (0) | 2024.01.11 |
#13 비동기 SQLModel (0) | 2024.01.08 |