본문으로 바로가기

[SQLAlchemy] Imperative Mapping

category Language/Python 2023. 11. 5. 20:14
728x90
반응형

목차

     

    개요 

    ORM이 제공하는 가장 중요한 기능은 영속성 무지(Persistence Ignorance)다.

    도메인 모델이 데이터를 어떻게 적재하는지 또는 어떻게 영속화하는지에 대해 알 필요가 없다는 의미다. 영속성 무지가 성립하면 특정 데이터 베이스 기술에 도메인이 직접 의존하지 않도록 유지할 수 있다.

    - 파이썬으로 살펴보는 아키텍처 패턴 (64p)

     

    SQLAlchemy를 사용하는 Application에서 Entity를 사용한다면 이는 곧 Declarative Mapping Style로 작성된 ORM Model과 연관될 가능성이 높다.

     

    그러나 Application에서 Entity가 "도메인 모델"로써 표현이 되어야 한다면 이는 다른 방법을 취해야 한다. 이러한 생각을 가지게 된 배경은 위의 인용에 적어놓은 "파이썬으로 살펴보는 아키텍처 패턴"이라는 책에서 해당 내용을 읽게된 이후이다.

     

    따라서 이 글에서는 SQLAlchemy에서 ORM Model을 사용할 수 있는 또 다른 형태인 Imperative Mapping Style을 다루려고 한다.

     

    조사해본 결과 SQLAlchemy의 버전에 따라 Imperative Mapping Style을 적용하는 방법에 차이가 있기에 SQLAlchemy 1.4.5 버전을 기준으로 Imperative Mapping Style을 어떻게 사용하는지에 다루려고 한다.

     

    1.  Declarative Mapping Style과  Imperative Mapping Style 

    SQLAlchemy의 ORM Model의 선언 방식에는 두 가지 방법이 존재한다.

     

    첫 번째는 선언형 매핑(Declarative Mapping Style)이며 두 번째는 명령형 매핑 스타일(Imperative Mapping Style)이다.  

     

    앞서 언급했듯 구글링이나 SQLAlchemy의 튜토리얼은 대부분 Declarative Mapping Style이다. 이 방식은 ORM Model을 선언하면 선언된 Model을 통해 ORM을 작성하여 Query를 날리는 등의 방식이다. 한 마디로 Django의 ORM과 같은 방식이다. 

     

    그러나 Imperative Mapping Style은 다르다. 직접 정의한 Class를 데이터베이스 테이블과 Mapping 시켜주는 명시적인 단계가 존재하며 직접 정의한 Class를 통해 Query를 날리는 게 가능하다.

     

    2. Imperative Style을 알아보자.

    Imperative Style을 간단히 표현하면 다음과 같다.

    DataBase Table과 개발자가 정의한 Class를 SQLAlchemy가 제공하는 Registry를 통해 연결하는 것이다. 이를 코드로 표현하면 다음과 같다.

    # DataBase의 Table로부터 생성된 SQLAlchemy Model
    from sqlalchemy.ext.declarative import declarative_base
    
    class Member(Base):
        __tablename__ = 'member'
       
        id = Column(INTEGER(1), primary_key=True)
    # 직접 정의한 Class
    import dataclasses
    
    @dataclasses.dataclass
    class MemberEntity:
    
    	id: Optional[int] = dataclasses.field()
    # Class와 Table을 연결해주는 Registry
    from sqlalchemy.orm import registry
    
    mapper_registry = registry()
    mapper_registry.map_imperatively(MemberEntity, Member)

     

    3.  SQLAlchemy Model은 어떻게 생성하는가?

    앞서 소개한 예시에서는 항상 SQLAlchemy Model이 최신화가 이루어져야 한다는 제약이 있다. alembic을 이용하여 DataBase 변경관리를 시도할 수는 있겠으나 복잡성이 높다. 그래서 필자는 작업 전에 항상 최신화를 시켜두는 방법을 택했다. 이를 편리하게 해주는 방식에는 "sqlacodegen"라는 도구를 사용할 수 있다.

    $ sqlacodegen mysql+pymysql://USER_NAME:PASSWORD@DB_HOST:DB_PORT/DB_NAME > ./orm.py

    이를 makefile에 등록해 두고 유용하게 써먹고 있다.

     

    4. Query는 어떻게 날리는가?

    만약 Declarative Mapping Style을 사용한다면 다음과 같이 ORM을 작성해야 한다.

    query = self.session.query(Member).filter(Member.member_id == member_id)

    그러나 Imperative Mapping Style로 Table과 Class를 연결했기에 이제는 Class를 통해 Query를 날리는 게 가능하다.

    query = self.session.query(MemberEntity).filter(MemberEntity.member_id == member_id)

     

     

    5. Imperative Style의 특이점

    "파이썬으로 살펴보는 아키텍처 패턴"이라는 책에서는 도메인 모델과 저장소와 관련된 관심사를 모델로부터 분리해야 된다고 언급되어 있다. 그러나 실제로 이를 적용하려다 보니 마주한 문제들은 다음과 같다.

    5.1 Type Hint의 문제 

    이는 SQLAlchemy의 공식 문서에도 드러난 있는 문제이다. 다음은 해당 내용을 인용한 것이다.

    The imperative mapping form is a lesser-sed form of mapping that originates from the very first releases of SQLAlchemy in 2006. It’s essentially a means of bypassing the Declarative system to provide a more “barebones” system of mapping, and does not offer modern features such as PEP 484 support. As such, most documentation examples use Declarative forms, and it’s recommended that new users start with Declarative Table configuration.

    명령형 매핑 형식은 2006년 SQLAlchemy의 첫 번째 릴리스에서 시작된 덜 사용되는 매핑 형식입니다. 이는 본질적으로 보다 "기본적인" 매핑 시스템을 제공하기 위해 선언적 시스템을 우회하는 수단이며 최신 기능을 제공하지 않습니다. PEP 484 지원과 같은. 따라서 대부분의 문서 예제는 선언적 형식을 사용하며 신규 사용자는 선언적 테이블 구성으로 시작하는 것이 좋습니다.

     

    이는 SQLAlchemy 2.0 버전을 기준으로 기재되어 있는 내용이다. 위 내용이 딱히 와닿지는 않았지만 코드를 작성하다 보니 다음과 같은 부분에서 Type Hint에 문제가 있다는 점을 발견했다.

    위 코드는 사이드 프로젝트에서 사용 중인 Repository 계층에 정의된 Method 중 하나이다. SQL의 "IN" 구문에 대한 사용 사례이다.

     

    위에서 발생한 문제는 FashionChinguEntity는 dataclass로 정의된 속성이고 Python의 built-in type으로만 이루어져있기에 SQLAlchemy의 Column 객체에서 사용할 수 있는 in_() 메서드를 사용하려다보니 Type Hint에 대한 Warning이 발생하고 있다.

     

    5.2 "1.4"에서 "2.0"으로 변경을 한다면...

    지금까지는 SQLALchemy 1.4.5에 기반하여 설명했다. 그러나 SQLAlchemy 2.0에서는 Imperative Mapping 방식을 다른 형태로 코드를 작성해야 한다. 먼저 ORM Model을 생성하는 도구가 다르다.

    # https://pypi.org/project/sqlacodegen-v2/
    ╰─$ sqlacodegen_v2 --generator tables mysql+pymysql://USER_NAME:PASSWORD@DB_HOST:DB_PORT/DB_NAME > /orm2.p

    아래는 위 명령으로부터 생성된 결과이다.

    from sqlalchemy import Column, MetaData, Table
    from sqlalchemy.dialects.mysql import INTEGER
    
    metadata = MetaData()
    
    t_member = Table(
        'member', metadata,
        Column('id', INTEGER(1), primary_key=True),
    )

    Registry도 다음과 같이 변경해야 한다.

    mapper_registry = registry()
    mapper_registry.map_imperatively(MemberEntity, t_member)

     

    5.3 Imperative Mapping Style의 실용성?

    Imperative Style에 관련하여 SQLAlchemy의 github repository의 discussions에서 다음과 같은 내용을 발견했다.

    SQLAlchemy의 maintainer 개발자가 2.0에서는 제거하려고 했지만 많은 사람들이 사용하고 있기에 그러지 못했다를 언급하고 있다. 링크를 타고 들어가 Discussions을 살펴보면 "zzzeek"라는 개발자는 굳이 Class를 하나 더 만들어서 사용하는 것에 대해 실용성을 느끼지 못했다고 한다.

     

    그러나 재밌는 점은 SQLAlchemy의 또 다른 maintainer는 상황에 맞는 방식을 선택하면 된다고 말한다.

     

    6. 마치며

    Imperative Mapping 방식을 처음 알게 된 건 "파이썬으로 살펴보는 아키텍처 패턴"이라는 책이다. 그 당시에는 Python에서  Data Mapper라는 것을 구현하기 위한 방법 중 일부라는 것으로 받아들였다.

     

    그러나 지금은 Application 안에서 Entity와 ORM Model은 앞서 언급했듯 "관심사의 분리" 그리고 "역할"의 분리를 적용해야 한다는 측면이 강하다고 생각한다. 궁극적으로 DataBase가 변경되어도 웬만해서는 사용할 수 있는 코드를 작성하는 것을 지향하고 있기 때문이다.

     

    그러나 "실용성" 측면에서는 SQLAlchemy Repository의 Discussions에서 나온 내용과 어느 정도 합치한다.

     

    결국 상황에 맞춰 어떤 스타일을 적용할 것인지는 개발하는 사람의 몫이 아닐까 싶다.

     


     

    728x90
    반응형