[FastAPI] 8. 비동기 처리에서 SQLAlchemy의 scoped_session이 문제가 되는 이유

반응형

아래의 포스트에서 FastAPI가 SQLAlchemy와 연동하였을 때 비동기적으로 처리하는 부분이 미흡하다는 단점에 대해 이야기했었던 적이 있습니다.

 

2020.12.27 - [Programming/Python] - [FastAPI] 2. SQLAlchemy를 이용한 간단한 CRUD API 만들기

 

[FastAPI] 2. SQLAlchemy를 이용한 간단한 CRUD API 만들기

이번 글에서는 ORM에 대한 사용 방법에 대해 알아보도록 하겠습니다. ORM은 Object Relation Mapping의 약자로 객체를 이용해서 데이터베이스 Entity에 접근하는 방법입니다. 보통 애플리케이션 레벨에서

blog.neonkid.xyz

이 부분에서 다룬 SQLAlchemy의 scoped_session에 대해 좀 더 이야기해보고, 어떤 부분이 비동기 처리에 대해 미흡한 부분이 있는지 더 개선할 수 있는 방법은 어떤 것들이 있는지 이야기해보고자 합니다.

 

 

 

 

scoped_session

먼저 scoped_session은 SQLAlchemy에서 단일의 DB Connection 하나를 이용해서 세션을 만들 수 있는 매커니즘 중 하나입니다. SQLAlchemy 문서에서 scoped_session은 아래와 같이 정의되어 있습니다.

 

-> scoped_session에서 생성되는 session은 스레드 단위로 생성이 된다.

 

파이썬의 스레드에 대해서 잘 모르겠다면, 스레드에 대한 개념을 먼저 파악하고 오시는걸 추천합니다. 간단히 설명드리자면 파이썬의 스레드는 "그린 스레드" 입니다. 여기서 그린 스레드란, System-Level의 스레드가 아닌 User-Level의 스레드를 이야기 합니다.

 

실제로 그렇게 동작하는지 한 번 실행해보도록 하겠습니다. 메인 스레드만을 사용하여 위와 같이 10개의 반복문을 주고 같은 세션을 주고 있는지를 한 번 확인해봤습니다.

 

문서대로 같은 스레드에서는 같은 세션을 주고 있음을 알 수 있습니다.

 

이번에는 Thread를 직접 만들어서 테스트해보도록 하겠습니다.

 

문서에 나와 있는대로 스레드마다 다른 세션의 객체가 생성되고 있음을 알 수 있습니다.

 

 

 

 

비동기 처리에서 scoped_session의 문제점

그렇다면 scoped_session이 비동기 처리에 있어서 왜 문제가 되는 것일까요? 그 이유는 바로 파이썬이 비동기 처리하는 원리에 있습니다.

 

Python에서 비동기 작업을 코루틴(Coroutine)이라고 말합니다. 코루틴은 Python 뿐만 아니라 Kotlin에도 존재하는 비동기 처리 메커니즘이지만 파이썬에서는 Event Loop가 중앙에 존재하고 이 Looper가 여러 요청을 받아 동시 처리할 수 있도록 지원합니다.

 

여기서 중요한 점은 위의 코루틴 그림은 하나의 스레드 즉, 싱글 스레드(Single Thread)로 동작한다는 것입니다. 마치 Node.js와 같지요. 그렇기 때문에 scoped_session을 사용할 경우 동시 요청이 들어왔을 때 동일한 세션을 사용한다는 것입니다.

 

이게 왜 문제일까요? 하나의 스레드에서 여러 가지의 일을 동시에 처리하기 때문에 동일한 세션 객체를 사용하게 되고, 해당 객체에 만약 Exception이 발생하게 된다면 다른 요청에서 사용한 session 객체에서도 똑같은 문제가 발생합니다. 즉 오류 전파의 원인이 되기도 하는 것이죠.

 

여기서 주의해야할 점은 비동기 처리를 이용했을 때 공유되는 세션을 사용한다 할지더라도 SQLAlchemy에서 제공하는 Session 객체는 동시에 처리되지 않습니다. 따라서 비동기 처리에 미흡할 수 밖에 없으며 다음 요청이 시작되기 전 이전 요청이 끝날 때까지 지연될 수 있도록 지원만 한다는 것을 알아두고 갑시다.

 

 

 

 

AsyncIterable

단순하게 생각해봅시다. 하나의 Connection에서 클라이언트가 들어오는 요청을 받기 위해 동기 처리를 하는 경우라면 세션을 공유해서 사용해도 먼저 들어온 요청이 끝난 다음 세션을 닫고, 다시 재활용하여 사용하기 때문에 동기 처리에 대해서는 문제가 없습니다.

 

그러나 비동기 처리는 하나의 요청이 진행되고 있는 중간에도 공유하고 있는 세션 객체를 사용하기 때문에 이전 요청에서 오류가 발생하면 해당 객체에서 발생한 오류이므로 이 객체를 사용하고 있는 모든 요청에 영향을 미치는 것입니다.

 

그렇다면 각 Context 별로 ThreadLocal처럼 별도의 세션을 가져야하는데요. FastAPI에서는 Depends를 사용하여 구현해 볼 수 있습니다.

 

먼저 기존에 사용했던 scoped_session 코드를 지우고, get_db_session 함수에서 직접 Session을 생성하도록 합니다. 이 때 생성할 때 컨텍스트가 순차적으로 세션을 생성하고 이를 보관할 수 있도록 파이썬의 추상 클래스인 Iterable을 사용합니다. 그런데, 우리는 비동기 방식으로 세션을 가져올테니 AsyncIterable을 사용해줍시다.

 

 

 

 

Connection Resource

커넥션을 전역 변수로 사용했는데, 전역 변수로 사용하게 되면 연결이 끊어지는 시점의 콜백 함수를 지정하는 데 어려움이 생깁니다. 따라서 FastAPI에서 제공하는 startup, shutdown 콜백 함수를 이용하여 애플리케이션이 종료 되었을 때 커넥션을 정상적으로 종료할 수 있도록 코드를 개선해줍시다.

 

먼저 커넥션을 생성하는 코드, 그리고 Dependency Injection으로 커넥션을 가져오는 함수, 커넥션을 종료하는 함수 이렇게 3가지를 만들어줍니다.

 

그러고 난 다음 FastAPI의 on_event 데코레이터를 사용하여 콜백 함수를 만들고 해당 함수에 위에서 작성한 함수를 넣어주도록 합니다.

 

이렇게 하면 애플리케이션이 종료되면서 안전하게 커넥션을 닫을 수 있고, 하나의 커넥션으로 같은 세션을 공유하지 않기 때문에 이전 요청에서 트랜잭션 오류 등이 발생해도 다른 요청에 영향을 주지 않게 됩니다.

 

 

 

 

마치며..

scoped_session의 문제점에 대해 간략하게 살펴봤습니다. FastAPI를 사용하는 것은 비동기 처리 외에도 여러방면이 존재하지만 비동기 처리하는 입장에서 완벽한 비동기 처리는 어렵다 하더라도 이를 이용하는 순간에 치명적인 오류가 발생한다면 이는 반드시 고쳐야 할 부분이라고 생각합니다.

 

하지만 이러한 방법을 사용했다고 하여 SQLAlchemy에서 DB 처리시 동시에 처리될 수 있는 것은 아닙니다. 단지 공유된 세션을 사용함으로써 공유하는 세션 사이 두 개의 요청 중 하나의 요청이라도 문제가 발생하면 문제가 없는 다른 요청에도 같은 영향을 발생하기 때문에 디버깅 등이 어려워지는 문제점을 초래하는 것을 막기 위한 수단일 뿐입니다.

 

다음 포스트에서는 asyncpg 등을 활용하여 Persistence Layer에서 비동기 처리하는 방법에 대해 알아보도록 하겠습니다.

 

 

 

 

참고: https://github.com/tiangolo/fastapi/issues/726

반응형

Tistory Comments 2

  • hihi

    https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html?highlight=async_scoped#using-asyncio-scoped-session

    async_scoped_session() 이 1.4.19 버전에 업데이트 되었습니다 ~