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 애플리케이션을 만들 수 있습니다.