Language/Python

[SQLAlchemy] dependency-injector로 SQLAlchemy Session 다루기

j4ko 2023. 11. 21. 01:44
728x90
반응형

목차


    개요

    FastAPI에서 SQLAlchemy Session을 사용하는 예제들을 살펴보면, 주로 contextmanager를 활용한 방법들이 계속해서 등장한다. 이러한 예제들은 주로 FastAPI에서 제공하는 Depands 함수를 이용하여 Dependency Injector를 구현한 형태이다.

     

    그러나 "dependency-injector"라는 DI 라이브러리를 통해서도 DI를 사용할 수 있는데 이 글에서는 “dependency-injector”를 이용해 SQLAlchemy Session을 FastAPI에서 사용한 방법을 기술하려 한다.

     


    dependency-injector library

    객체 지향 언어에서 뜻하는 의존성 주입은 꽤나 포괄적이고 그 내용도 심오하지만 기본적으로는 “객체가 필요로 하는 어떤 것을 외부에서 전달해 주는 것”으로 볼 수 있다. “dependency-injector”는 이러한 구현을 가능하게 도와주는 라이브러리이다. 상세한 정보는 관련 문서에서 참고하자.

    https://python-dependency-injector.ets-labs.org/#

     

    Dependency Injector — Dependency injection framework for Python — Dependency Injector 4.41.0 documentation

    Dependency Injector — Dependency injection framework for Python Dependency Injector is a dependency injection framework for Python. It helps implementing the dependency injection principle. Key features of the Dependency Injector: Providers. Provides Fac

    python-dependency-injector.ets-labs.org

    dependency-injector를 어떻게 사용하는지에 대해서는 이 글을 쓰려는 목적에서 벗어나기에 최대한 기술하지는 않겠다.


    dependency-injector로 SQLAlchemy Session 사용하기

    dependency-injector로 SQLAlchemy session을 사용하기 전 SQLAlchemy session과 engine을 이용하는 코드는 다음과 같이 작성했다.

    class SQLALchemyEngineFactory:
    
        @classmethod
        def get_engine(cls, url: URL):
            return create_engine(
                url,
                pool_pre_ping=True,
                pool_recycle=5,
                pool_size=5,
                pool_timeout=15,
                echo=True
            )
    
    class SQLALchemySessionFactory:
    
        @classmethod
        def get_session(cls, engine):
            return scoped_session(sessionmaker(
                bind=engine,
                expire_on_commit=False,
                autocommit=False,
                autoflush=False,
            ))

    이를 dependency-injector를 이용해 SQLAlchemy session을 Conainer를 통해 얻을 수 있게 만들어보자.

    from dependency_injector import containers, providers
    
    class SesssionContainer(containers.DeclarativeContainer):
        engine = providers.Dependency()
    
        session = providers.Singleton(
            SQLALchemySessionFactory.get_session,
            engine=engine
        )

    SessionContainer에서 session을 사용하려면 아래와 같은 코드가 필요하다.

    sa_engine = SQLALchemyEngineFactory.get_engine(url=preset_url)
    
    session_container = SesssionContainer(engine=sa_engine)
    session_container.wire(modules=[__name__])

    이렇게 잡아놓은 코드를 어떤 클래스에서 사용할 때는 다음과 같이 만들 수 있다.

    class Conetext:
        session = Provide[SesssionContainer.session]
    

    Singleton Provider를 사용했기 때문에 같은 session을 이용하는 걸 확인해 보자.

    for x in range(100):
    	print(Conetext.session)
    
    """ OutPut
    <sqlalchemy.orm.scoping.scoped_session object at 0x1046d60e0>
    <sqlalchemy.orm.scoping.scoped_session object at 0x1046d60e0>
    <sqlalchemy.orm.scoping.scoped_session object at 0x1046d60e0>
    """

     


    FastAPI에서 사용하다 겪은 증상

    위에서는 간략히 dependency-injector에서 SQLAlchemy session을 담아 특정 class에서 꺼내서 사용하는 형태의 예제를 적었다. 그러나 실제로 FastAPI를 통해서 사용할 때는 문제가 되는 부분이 존재했다.

     

    문제는 repository에서 orm을 사용한 뒤 상위 계층에서 이를 transaction으로 한 번에 묶어서 사용할 때 일어났다.

    위 도식은 FastAPI에서 문제가 일어났던 코드의 형태를 나타내는 간략하게 표현한 것이다. UseCase라는 클래스의 특정 메서드의 동작에서 여러 Repository에 걸쳐 쿼리를 발생시키고 이를 Transaction으로 묶어서 commit을 처리하는 형태이다.

     

    그러나 위와 같이 사용한 경우 N+1 문제가 발생했다. 한 3~4일 동안 삽질한 경우 session을 생성할 때 사용할 수 있는 옵션인 expire_on_commit을 False로 설정하지 않아서 일어난 문제였다.

     


    expire_on_commit 은 무엇인가?

    expire_on_commit에 대한 문서는 공식 문서에서 찾기보다 chatgpt한테 물어봤다.

     

    위의 설명대로라면 expire_on_commit이 True인 경우에는 session.query를 통해 변수에 바인딩된 결과를 commt()을 하고 다시 접근하는 경우 쿼리가 두 번 발생한다는 뜻인데 정말 그런지 확인해 보자.

    # SQLAlchemy.get_session의 expire_on_commt이 True인 상태
    query = Conetext.session.query(MemberEntity).first()
    
    Conetext.session.commit()
    
    print(query.member_id)
    

    위와 같은 코드를 작성했을 때 결과는 다음과 같았다.

    실제로 쿼리를 두 번 날린다. 이를 토대로 생각해 보면 repository에서 session.query를 통해 orm을 사용한 후 commit 처리 때문에 문제가 생긴 걸로 추정할 뿐이다.


    마치며

    이번 글은 정리를 하면서도 스스로 미심쩍은 부분이 남아있다. dependency-injector에서 SQLAlchemy Session을 사용하는 다른 방법이 존재할 것도 같고 SQLAlchemy의 N+1을 방지하려면 expire_on_commit을 False로 한 상황에서 명시적인 commit 방법을 강제해야 하는지 등의 부분들이다.

     

    그러나 일단 기록은 해두기로 정했다. 시간이 지나고 다시 이 글을 봤을 때 이렇게 사용한 것에 대한 경험적 판단이 어떤지 기대해보고자 한다.

    728x90
    반응형