내용이 알차지도 아닌지라 포스팅은 꾸미지 않습니다.
Quiz 모델에서 `game`이라는 @property 메서드를 만들고 난 뒤, 테스트 코드에서 사용했더니 아래와 같은 오류가 생겼다.
sqlalchemy.orm.exc.DetachedInstanceError: Parent instance <GameScreenshot at 0x1045fc8b0> is not bound to a Session; lazy load operation of attribute 'game' cannot proceed (Background on this error at:
https://sqlalche.me/e/20/bhk3)
이는 Quiz모델에 세션이 바인딩 되지 않아서 발생한 오류였다.
사실 이 오류는 세션 관리에도 관련이 있는 문제였다.
내 테스트 코드에서는 기본 데이터를 저장하기 위해 create_random_quiz라는 헬퍼 함수를 정의해서 사용하고 있었다.
헬퍼 함수는 아래와 같이 생겼다.
async def create_random_quiz(screenshots: list[GameScreenshot] | None = None) -> Quiz:
if screenshots:
assert len(screenshots) == QUIZ_SCREENSHOT_COUNT
# 하나의 게임에만 속해 있어야 한다.
assert len(set(s.game_id for s in screenshots)) == 1
else:
game = await create_random_game()
screenshots = await asyncio.gather(*[create_random_game_screenshot(game.id) for _ in range(5)])
with Session(engine) as session:
quiz = Quiz(screenshots=screenshots)
session.add(quiz)
session.commit()
session.refresh(quiz, ["screenshots"])
return quiz
세션은 이 구간에서만 생성되고 사용되는 구조다.
먼저 test_xxx로 시작하는 테스트 함수는 세션 관리에 대한 책임을 주고 싶지 않았고,
세션을 생성하면 Engine의 커넥션을 점유한다고 생각했다. (사실 항상 점유하는 건 아니다!)
의도는 좋았지만, 역시 문제가 있었다. Quiz의 lazy loading되는 attribute는 세션이 바인딩 되어야 접근할 수 있었다.
그래서 아래와 같이 refresh할때 lazy loading attribute도 같이 가져오도록 하였다.
`session.refresh(quiz, ["screenshots"])`
여기까지는 좋았지만 game 이라는 @property는 refresh에서 지원해주지 않았던 것이다.
사실 이 방법에서는 사용범위가 같아야 하지만 어긋나버린 두 객체가 있다.
Quiz라는 모델과 Session.
세션을 헬퍼 함수에만 가두고 싶다면 Quiz라는 모델을 반환할게 아니라 딕셔너리 혹은 DTO용 dataclass나 pydantic객체를 만들어 반환했어야 했다.
그럼 그렇게 할까? -> 놉! 나는 테스트에 이렇게 많은 것을 두고 싶지 않다.
dto도 lazy loading되는 attribute에 따라 다르게 만들어야 하고, 참으로 복잡할 것이다.
그래서 세션은 pytest fixture에서 만들어서 test_xxx에 주입하는 방식을 다들 사용하나보다.
그리고, 세션이 커넥션을 항상 점유하는 것은 사실이 아니였다.
아래는 sqlalchemy의 공식 사아트에서 가져온 글이다.
The Session in its most common pattern of use begins in a mostly stateless form. Once queries are issued or other objects are persisted with it, it requests a connection resource from an Engine that is associated with the Session, and then establishes a transaction on that connection.
세션은 데이터를 영속할때 혹은 쿼리를 실행할때 커넥션을 가져온다고 한다.
When the transaction ends, the connection resource associated with the Engine is released to the connection pool managed by the engine
또, 커밋이나 롤백한 후에는 커넥션을 놓아준다.
때문에, 커넥션 자원에 대해 걱정할 필요가 없다. 세션은 모델의 사용 범위를 따라가야 한다.
그래서 결국 이렇게 바꿨다.
async def create_random_quiz(session: Session, *, screenshots: list[GameScreenshot] | None = None) -> Quiz:
if screenshots:
assert len(screenshots) == QUIZ_SCREENSHOT_COUNT
# 하나의 게임에만 속해 있어야 한다.
assert len(set(s.game_id for s in screenshots)) == 1
else:
game = await create_random_game(session)
screenshots = await asyncio.gather(
*[create_random_game_screenshot(session, game_id=game.id) for _ in range(5)]
)
quiz = Quiz(screenshots=screenshots)
session.add(quiz)
session.commit()
session.refresh(quiz)
return quiz
'프로그래밍 > 스팀 게임 퀴즈' 카테고리의 다른 글
# 12 백엔드 코드를 비동기로 바꾸다. (0) | 2024.01.08 |
---|---|
#8 HttpUrl vs str (0) | 2023.12.30 |
#6 퀴즈 정답 제출은 어떻게 할까? (0) | 2023.12.28 |
#5 테스트 데이터 만드는 법 (0) | 2023.12.27 |
#3 DB 세션에 대한 문제 (1) | 2023.12.25 |