개발 이모저모

SQLAlchemy 2.0 - Major Migration Guide

발짜개 2025. 1. 19. 05:32

SQLAlchemy란 무엇인가

SQLAlchemy는 Python에서 가장 널리 사용되는 데이터베이스 도구 중 하나로, SQL 데이터베이스를 효과적으로 다루기 위한 SQL 도구 및 Object-Relational Mapping(ORM) 라이브러리입니다. SQLAlchemy는 Python 개발자들에게 강력하고 유연한 데이터베이스 인터페이스를 제공합니다.

SQLAlchemy 등장 배경

  1. SQL의 복잡성 관리: 이전에는 데이터베이스와 상호작용하기 위해 직접 SQL 쿼리를 작성하는 것이 일반적이었으나, 복잡한 쿼리를 간편하게 사용하고 쉽게 유지보수하기 위해 SQLAlchemy가 등장했습니다.
  2. 데이터베이스 데이터와 코드상의 객체 맵핑의 필요성: SQLAlchemy는 데이터베이스의 구조를 Python 객체로 추상화해, 데이터베이스의 테이블과 레코드를 코드로 쉽게 표현할 수 있으며, 객체 지향 프로그래밍 스타일로 데이터베이스를 다룰 수 있게 해 줍니다.
  3. 데이터베이스 독립성 제공: SQLAlchemy는 다양한 데이터베이스(DBMS)에 대한 지원을 제공하며, 데이터베이스 간에 전환할 때 코드를 최소한으로 수정하도록 설계되었습니다.

SQLAlchemy의 주요 특징과 장점

  1. Core: SQLAlchemy는 고급 SQL 표현식을 작성하고 실행할 수 있는 강력한 SQL Expression Language를 제공합니다.
  2. ORM(Object-Relational Mapper): 데이터베이스 테이블을 Python 클래스와 매핑하여 객체 지향적으로 데이터를 다룰 수 있습니다.
  3. 데이터베이스 독립성: 다양한 데이터베이스 엔진(MySQL, PostgreSQL, SQLite 등)을 지원합니다.
  4. 고성능: 효율적인 데이터 처리 및 쿼리 최적화를 제공합니다.
  5. 유연성: SQLAlchemy는 SQL Expression Language와 ORM을 독립적으로 또는 함께 사용할 수 있도록 설계되었습니다.

 

SQLAlchemy 2.0

SQLAlchemy 1.X버전에서 와 SQLAlchemy 2.0으로 매끄럽게 업데이트를 하는 방법에 대해서 알아봅시다. 2.0에서는 Core 및 ORM 구성 요소의 다양한 주요 사용 패턴이 크게 바뀌어서 버전 업데이트 시 코드 수정이 필수적입니다. 예를 들어 1.x 스타일에서

session.query(User).\\
  filter_by(name="some user").\\
  one()

다음과 같은 2.x 스타일로 변했습니다.

session.execute(
  select(User).
  filter_by(name="some user")
).scalar_one()

2.0에서 변화된 것들

2.0에서의 새로운 변화는 다음과 같습니다.

  • 새로운 ORM statement 패러다임
  • Core 및 ORM의 SQL 캐싱
  • 새로운 Declarative 기능 및 ORM 통합
  • 새로운 Result 객체
  • select() 및 case()에서 위치 인수 허용
  • Core 및 ORM에 대한 asyncio 지원

아래와 같은 기능들은 2.0부터 더 이상 제공되지 않습니다.

  • Bound MetaData 및 연결 없는 실행(connectionless execution)
  • Connection 레벨에서의 autocommit
  • Session.autocommit 매개변수/모드
  • select()의 List 및 Keyword 인자
  • Python 2 지원

이러한 변경 사항을 수용하기 위해서는 1.4에서 “future API”의 도움을 받아 매끄럽게 2.0으로 전환할 수 있습니다. 더 자세한 변화와 예시는 공식 문서에서 확인해 볼 수 있습니다.

 

Migration Guide

2.0의 새로운 API 및 기능은 사실 1.4부터 포함되어 있으며 사용 가능합니다. 마이그레이션 단계를 간략하게 설명하자면, 모든 경고 플래그가 켜진 상태에서 1.4 버전이 잘 실행되고 2.0 지원 중단 경고가 발생하지 않으면 2.0과 대부분 상호 호환된다는 것이고, 이를 통해 2.0으로 부드러운 버전 업그레이드가 가능하도록 합니다. 마지막으로 2.x 릴리즈에 대해서 코드를 테스트해야 합니다.

전제 조건 1. 작동하는 1.3 버전

첫 번째 단계는 1.3 버전에서 SADeprecationWarning 클래스에서 발생하는 경고 없이 프로그램이 실행되거나 모든 테스트를 통과하는 것입니다. 1.4에는 이전 버전과 달라진 경고가 있으며, 특히 1.3에 도입된, relationship.viewonly 와 relationship.sync_backref flag의 작동 방식이 일부 달라졌습니다.

전제 조건 2. 작동하는 1.4 버전

다음 단계는 SQLAlchemy 1.4 버전에서 프로그램이 잘 실행되는 것입니다. 대부분의 경우 1.4까지는 문제없이 실행될 것입니다.

만약 매끄럽게 실행되지 않는다면 다음과 같은 사항을 확인해 보는 것이 좋습니다.

  • URL 객체는 변경 불가능(immutable)
  • SELECT 문은 더 이상 암시적으로 FROM 절로 간주되지 않음
  • select().join() 및 outerjoin()은 하위 쿼리를 생성하는 대신 현재 쿼리에 JOIN 조건을 추가
  • 대다수의 Core 및 ORM statement 개체가 컴파일 단계에서 만들어지며 유효성 검사를 수행. 따라서 오류 메시지가 구성 시점이 아닌 컴파일/실행 시까지 표시되지 않을 수 있음

1단계 - 최소 Python 3.7 이상

2020년에 Python 2가 EOL(End of Life)을 맞이했습니다. SQLAlchemy 2.0을 사용하려면 애플리케이션이 최소한 Python 3.7에서 실행 가능해야 합니다. 1.4는 파이썬 3.6 이상을 지원합니다.

2단계 - RemovedIn20Warnings 켜기

SQLAlchemy 1.4에는 레거시 패턴을 알려주는 deprecation warning 시스템이 도입되었습니다. 1.4에서 환경 변수 SQLALCHEMY_WARN_20을 true 또는 1로 세팅하여 사용 가능합니다. 실행 시 warning이 보이지 않는다면 혹시 경고를 억제하는 필터가 적용되어 있는지 확인합니다.

3단계 - 모든 RemovedIn20Warnings 해결

이때 한 번에 모든 warning을 해결하려 하기보다 한 번에 한 종류씩 해결하는 것이 좋습니다. 특정 warning 하위 집합을 고른 후 그것만 빼고 나머지는 무시하도록 하는 필터를 설정해서, 한 번에 하나씩만 작업합니다. 코드를 수정한 후 특정 경고가 더 이상 발생하지 않으면 해당 필터를 제거하고, 다음 warning으로 넘어가 작업하고, 프로그램이 RemoveIn20Warning 없이 매끄럽게 실행될 때까지 반복합니다.

4단계 - Engine 객체에서 future 플래그 사용

future 플래그를 켜놓으면 해당 객체나 함수는 새로운 2.0 API를 완전히 지원하고, deprecated 된 기능들은 제외하여 실행합니다. 예를 들어 Engine 객체를 생성할 때 create_engine() 함수에 future=True 플래그를 전달하여 사용할 수 있습니다. Engine과 Connection 관련 RemovedIn20Warning 경고를 모두 해결한 후에 create_engine.future 플래그를 활성화할 수 있으며, 이때 오류는 발생하지 않아야 합니다.

5단계 - Session에서 future 플래그 사용

Session 객체 역시 2.0 버전에서 업데이트된 트랜잭션/커넥션 레벨 API를 제공합니다. 1.4 버전에서는 Session.future 플래그를 Session 또는 sessionmaker에 사용하여 이 API를 활성화할 수 있습니다. 플래그가 켜지면 다음과 같은 상태가 됩니다.:

  • Session은 연결에 사용할 엔진을 확인할 때 더 이상 bound metadata를 지원하지 않습니다. 즉, Engine 객체를 생성자에게 전달해야 합니다(이때 Engine은 레거시나 2.x 스타일 객체 둘 다 가능).
  • Session.begin.subtransactions 플래그는 더 이상 지원되지 않습니다.
  • Session.commit() 메서드는 subtransaction을 조정하려 하지 않고 항상 데이터베이스에 COMMIT을 내보냅니다.
  • Session.rollback() 메서드는 subtransaction을 그대로 유지하려고 하지 않고 항상 전체 트랜잭션 스택을 한 번에 롤백합니다.

Session은 1.4에서 더욱 유연한 생성 패턴을 지원하며, Connection 객체에서 사용하는 패턴과 밀접하게 일치합니다. 주요 특징으로는 Session을 콘텍스트 관리자로 사용할 수 있다는 점이 있습니다.

from sqlalchemy.orm import Session

with Session(engine) as session:
    session.add(MyObject())
    session.commit()

 

또한 sessionmaker 객체는 세션을 생성하고 한 블록에서 트랜잭션을 시작/커밋하는  sessionmaker.begin() 콘텍스트 매니저를 지원합니다:

from sqlalchemy.orm import sessionmaker

Session = sessionmaker(engine)

with Session.begin() as session:
    session.add(MyObject())

 

SQLALCHEMY_WARN_20=1로 설정하고 모든 경고가 켜져 있을 때 모든 테스트를 통과하고 애플리케이션이 잘 실행된다면 완성입니다!

6단계 - 명시적으로 형식화된 ORM 모델에 __allow_unmapped__=True 추가

SQLAlchemy 2.0에서는 ORM 모델의 PEP 484 type annotation(타입 힌트) 지원이 추가되었습니다. 이러한 annotation은 반드시 Mapped를 사용해야 합니다.

class sqlalchemy.orm.Mapped

매핑된 class에서 ORM 매핑된 attribute를 나타냅니다. Attribute가 올바르게 입력되도록 pylance 및 mypy와 같은 타입체커에 적절한 정보를 제공합니다. Mapped는 보통 mapped_class()나 relationship()등을 설정할 때 사용됩니다.

 

아래 코드에서는 Mapped를 사용하지 않은 클래스의 relationship() 필드의 타입 힌트가 List["Bar"]일 때 에러를 발생시킵니다.

Base = declarative_base()

class Foo(Base):
    __tablename__ = "foo"

    id: int = Column(Integer, primary_key=True)

    # This will raise Error!!
    bars: List["Bar"] = relationship("Bar", back_populates="foo")

class Bar(Base):
    __tablename__ = "bar"

    id: int = Column(Integer, primary_key=True)
    foo_id = Column(ForeignKey("foo.id"))

    # will raise
    foo: Foo = relationship(Foo, back_populates="bars", cascade="all")

 

Mapped를 사용하지 않는 모든 type annotation이 오류 없이 전달되도록 하려면 __allow_unmapped__ 속성을 사용합니다. 이러한 경우 새로운 Declarative System에서 annotation을 완전히 무시하게 됩니다.

아래 예시는 하위클래스에 __allow_unmapped__를 적용한 것입니다.

# qualify the base with __allow_unmapped__.  Can also be
# applied to classes directly if preferred
class Base:
    __allow_unmapped__ = True

Base = declarative_base(cls=Base)

# existing mapping proceeds, Declarative will ignore any annotations
# which don't include ``Mapped[]``
class Foo(Base):
    __tablename__ = "foo"

    id: int = Column(Integer, primary_key=True)

    bars: List["Bar"] = relationship("Bar", back_populates="foo")

class Bar(Base):
    __tablename__ = "bar"

    id: int = Column(Integer, primary_key=True)
    foo_id = Column(ForeignKey("foo.id"))

    foo: Foo = relationship(Foo, back_populates="bars", cascade="all")

7단계 - SQLAlchemy 2.0 릴리스에 대한 테스트

SQLAlchemy 2.0에는 이전 버전과 호환될 수 있도록 추가된 API 변경 사항이 있지만 그럼에도 불구하고 특정 케이스에서 문제가 생길 수 있습니다. 따라서 마지막 단계는 가장 최신 버전의 SQLAlchemy 2.x에 대한 테스트입니다.

 

마무리하며

이번 글에서는 SQLAlchemy 1.x 버전에서 2.x 버전으로 업그레이드하는 방법을 살펴봤습니다. 이전 회사에서 1.4 버전으로 업그레이드하는 것도 꽤나 많은 작업이 필요했는데요, 앞으로 2.x 버전을 써보는 것이 기대가 됩니다.