SQLAlchemy 2.0와 Python 데이터 클래스를 활용한 데이터베이스 상호 작용 현대화
Takashi Yamamoto
Infrastructure Engineer · Leapcell

서론: Python 데이터베이스 상호 작용의 새로운 시대
많은 Python 애플리케이션에서 데이터베이스 상호 작용은 기본적인 요구 사항입니다. 역사적으로 SQLAlchemy와 같은 객체 관계형 매퍼(ORM)는 개발자가 Python 객체로 데이터베이스 엔터티를 작업할 수 있도록 하는 강력한 추상화 계층을 제공했습니다. 매우 효과적이었지만, SQLAlchemy의 이전 버전은 특히 쿼리 구성 시 가파른 학습 곡선을 제시하기도 했습니다. SQLAlchemy 2.0의 출시는 더 큰 일관성, 명확성 및 더 직관적인 개발자 경험을 목표로 하는 중요한 발걸음을 내디뎠습니다. 동시에 Python의 dataclasses 모듈은 간단하고 불변적인 데이터 구조를 정의하는 데 선호되었습니다. 이 글에서는 SQLAlchemy 2.0의 select()의 현대적인 쿼리 스타일과 dataclasses의 우아함을 결합하여 Python 데이터베이스 작업을 극적으로 단순화하고 현대화하는 방법을 자세히 살펴보고, 더 읽기 쉽고 유지하기 쉬우며 강력한 코드를 작성하도록 합니다.
핵심 이해하기: SQLAlchemy 2.0의 select()와 Python 데이터 클래스
실제 예시로 들어가기 전에, 논의할 핵심 개념에 대한 명확한 이해를 확립해 보겠습니다.
SQLAlchemy 2.0의 select(): 이것은 SQLAlchemy 2.0의 SQL 표현식 언어의 초석입니다. 이전 버전의 보다 명령적인 쿼리 구성 메서드를 완전히 선언적이고 함수적인 API로 대체합니다. select() 구문은 ORM의 이점을 유지하면서 SQL 쿼리 구조를 더 밀접하게 반영하는 매우 조합 가능하고 명확하도록 설계되었습니다. 불변성을 강조하여 select() 객체의 각 메서드 호출은 새로운 수정된 select를 반환하여 예측 가능한 동작을 촉진합니다.
Python dataclasses: Python 3.7에 도입된 dataclasses 모듈은 주로 데이터를 저장하는 클래스의 __init__(), __repr__(), __eq__() 등의 메서드를 자동으로 생성하는 데 장식자와 함수를 제공합니다. 많은 사용 사례에서 기존 클래스보다 간단하고 namedtuple보다 간결합니다. 데이터베이스 상호 작용의 경우 dataclasses는 간단한 경우 전체 ORM 모델의 오버헤드 없이 데이터베이스 엔터티의 구조를 정의하는 깔끔한 방법을 제공하거나 쿼리 결과에 대한 일반 데이터 전송 객체(DTO)로 사용됩니다.
이제 이 두 가지 강력한 기능이 어떻게 통합되어 더 나은 데이터베이스 상호 작용 경험을 제공하는지 살펴보겠습니다.
현대적인 데이터베이스 작업: 조합의 힘
SQLAlchemy 2.0의 select()를 dataclasses와 함께 사용할 때 진정한 시너지가 나타납니다. SQLAlchemy는 역사적으로 ORM 매핑에 선언적 기본 모델을 사용하지만, dataclasses는 select() 문의 사용자 지정 결과 유형을 정의하는 데 사용될 수 있으며, 특히 특정 열이나 집계를 간단하고 잘 정의된 구조로 투영해야 할 때 사용됩니다. 이는 읽기 전용 작업이나 데이터 전송 객체를 전체 ORM 모델과 분리하려는 경우에 특히 유용합니다.
환경 설정
먼저, 시연 목적으로 간단한 데이터베이스와 일반 ORM 모델로 기본 SQLAlchemy 환경을 설정해 보겠습니다. 그런 다음 dataclasses를 사용하여 쿼리된 데이터를 나타내는 방법을 보여주겠습니다.
import os from dataclasses import dataclass from typing import List, Optional from sqlalchemy import create_engine, Column, Integer, String, select from sqlalchemy.orm import declarative_base, sessionmaker, Mapped, mapped_column # 선언적 모델의 기본 정의 Base = declarative_base() # 일반 SQLAlchemy ORM 모델 정의 class User(Base): __tablename__ = 'users' id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(50), nullable=False) email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) def __repr__(self): return f"<User(id={self.id}, name='{self.name}', email='{self.email}')>" # 쿼리 결과용 데이터 클래스 정의 @dataclass class UserInfo: id: int name: str email: str # 부분 결과용 더 간단한 데이터 클래스 정의 @dataclass class UserNameAndEmail: name: str email: str # 데이터베이스 설정 DATABASE_URL = "sqlite:///./test.db" engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # 테이블 생성 Base.metadata.create_all(bind=engine) # 세션 가져오기 헬퍼 함수 def get_db_session(): db = SessionLocal() try: yield db finally: db.close()
초기 데이터 삽입
예제 사용자를 몇 명 추가하여 데이터베이스를 채워 보겠습니다.
def seed_data(): db = next(get_db_session()) # 세션 가져오기 users_to_add = [ User(name="Alice", email="alice@example.com"), User(name="Bob", email="bob@example.com"), User(name="Charlie", email="charlie@example.com"), User(name="David", email="david@example.com"), ] for user in users_to_add: existing_user = db.query(User).filter_by(email=user.email).first() if not existing_user: db.add(user) db.commit() db.close() seed_data()
select()로 쿼리하고 UserInfo 데이터 클래스 인스턴스 반환하기
이제 select()를 사용하여 데이터를 가져오고 UserInfo 데이터 클래스로 자동 매핑하는 방법을 보겠습니다. 여기서 핵심은 select().scalars() 또는 select().all()을 mapping(UserInfo)와 함께 사용하거나 단순히 결과에서 데이터 클래스 인스턴스를 구성하는 것입니다.
def get_all_users_as_dataclass() -> List[UserInfo]: db = next(get_db_session()) try: # 개별 열을 선택한 다음 데이터 클래스를 구성할 수 있습니다. # 또는 ORM 객체에서 직접 매핑하는 경우 다르게 할 수 있습니다. # 열 투영을 데이터 클래스로 직접 하려면 행을 가져온 다음 압축을 풉니다. stmt = select(User.id, User.name, User.email) results = db.execute(stmt).all() # 데이터 클래스 인스턴스로 수동 매핑 user_info_list = [UserInfo(id=r.id, name=r.name, email=r.email) for r in results] return user_info_list finally: db.close() print("\n--- 모든 사용자를 UserInfo 데이터 클래스로 --- ") all_users_dataclass = get_all_users_as_dataclass() for user_info in all_users_dataclass: print(user_info) # 출력: # UserInfo(id=1, name='Alice', email='alice@example.com') # UserInfo(id=2, name='Bob', email='bob@example.com') # UserInfo(id=3, name='Charlie', email='charlie@example.com') # UserInfo(id=4, name='David', email='david@example.com')
UserNameAndEmail로 부분 데이터 투영하기
사용자 정보의 일부만 필요한 경우 어떻게 해야 할까요? select()는 이를 쉽게 처리하고 dataclasses는 이러한 부분 결과에 대한 깔끔한 대상을 제공합니다.
def get_user_names_and_emails() -> List[UserNameAndEmail]: db = next(get_db_session()) try: stmt = select(User.name, User.email).where(User.name.startswith("C")) results = db.execute(stmt).all() # 더 간단한 데이터 클래스로 매핑 name_email_list = [UserNameAndEmail(name=r.name, email=r.email) for r in results] return name_email_list finally: db.close() print("\n--- 사용자 이름 및 이메일(필터링됨)을 UserNameAndEmail 데이터 클래스로 --- ") partial_users = get_user_names_and_emails() for user_part in partial_users: print(user_part) # 출력: # UserNameAndEmail(name='Charlie', email='charlie@example.com')
select()로 필터링 및 정렬하기
select() 구문은 직관적이고 체인 방식으로 연결 가능한 모든 일반 SQL 작업을 지원합니다.
def get_users_by_id_range(min_id: int, max_id: int) -> List[UserInfo]: db = next(get_db_session()) try: stmt = ( select(User.id, User.name, User.email) .where(User.id >= min_id) .where(User.id <= max_id) .order_by(User.name) # 이름을 알파벳순으로 정렬 ) results = db.execute(stmt).all() return [UserInfo(id=r.id, name=r.name, email=r.email) for r in results] finally: db.close() print("\n--- ID 범위별 사용자 및 이름순 정렬 --- ") filtered_users = get_users_by_id_range(2, 3) for user_info in filtered_users: print(user_info) # 출력: # UserInfo(id=3, name='Charlie', email='charlie@example.com') # UserInfo(id=2, name='Bob', email='bob@example.com') (참고: id 2와 3의 순서는 이름에 따라 달라질 수 있으며, Bob이 Charlie보다 먼저 옵니다.)
잠시만, 출력을 다시 확인하겠습니다. Bob이 Charlie보다 먼저 옵니다. 이름별 알파벳순 정렬에 대한 출력은 올바릅니다.
이 접근 방식의 장점
- 가독성:
select()구문은 매우 표현력이 뛰어나며 SQL과 매우 유사하게 읽혀 어떤 데이터를 검색하는지 이해하기 쉽게 만듭니다.dataclasses는 예상되는 결과 구조에대한 명확하고 명시적인 정의를 제공합니다. - 타입 안전성:
dataclasses를 타입 힌트와 함께 정의함으로써 정적 분석의 이점을 얻어 코드가 데이터 타입을 올바르게 처리하고 런타임 오류를 줄입니다. - 분리: 쿼리 결과에
dataclasses를 사용하면 데이터 전송 객체를 ORM 모델과 분리할 수 있습니다. 이는 계층형 아키텍처에서 전체 ORM 엔터티를 노출하지 않고 계층 간에 더 간단하고 목적에 맞게 설계된 데이터 객체를 전달하려는 경우에 특히 유용합니다. - 유연성:
select()는 조인, 집계 및 하위 쿼리를 포함한 복잡한 쿼리 구성을 위해 매우 유연합니다.dataclasses는 이러한 쿼리의 모든 투영에 맞게 조정될 수 있습니다. - 현대적인 Python:
dataclasses와 SQLAlchemy의 의도된 2.0 스타일과 같은 최신 Python 기능을 활용하여 보다 관용적이고 미래 지향적인 코드를 작성합니다.
결론
SQLAlchemy 2.0의 select() 문과 Python dataclasses를 전략적으로 결합함으로써 개발자는 데이터베이스 상호 작용에 대해 고도로 현대화되고 타입이 안전하며 읽기 쉬운 접근 방식을 달성할 수 있습니다. 이 패턴은 쿼리 구성을 단순화하고, 명시적인 데이터 구조를 통해 코드 명확성을 향상시키며, 더 나은 아키텍처 분리를 촉진합니다. 이 스타일을 채택하면 보다 강력하고 유지 관리하기 쉬운 데이터베이스 기반 Python 애플리케이션을 만들 수 있습니다.

