파이썬 SQLAlchemy 2.0 기초: ORM 데이터베이스 테이블 매핑
SQLAlchemy 2.0의 DeclarativeBase, Mapped, Session과 select 문법으로 테이블 매핑과 CRUD, 관계 설정 및 트랜잭션 처리를 익힙니다.

SQLAlchemy 2.0 ORM은 파이썬 클래스를 데이터베이스 테이블에 매핑하고, 객체 생성·조회·수정으로 SQL 작업을 표현하게 해주는 도구입니다. 처음 배울 때는 DeclarativeBase로 매핑 기준을 만들고, Mapped와 mapped_column()으로 열을 선언한 뒤, Session 안에서 select()를 실행하는 2.0 스타일 흐름을 익히는 것이 핵심입니다.
SQLAlchemy 2.0 ORM 공식 빠른 시작 보기
핵심 요약
- ORM은 테이블의 행을 파이썬 객체로 다루지만 SQL과 트랜잭션 지식은 여전히 필요합니다.
- 2.0 선언형 매핑은
DeclarativeBase,Mapped,mapped_column()조합이 기본입니다. create_engine()은 연결 설정의 출발점이며 실제 연결은 필요할 때 확보됩니다.Session은 객체 상태와 트랜잭션을 관리하므로 작업 단위를 짧고 명확하게 유지합니다.- 개발 중에는
create_all()이 편리하지만 운영 스키마 변경에는 마이그레이션 도구를 검토해야 합니다.
1. ORM과 SQLAlchemy 2.0의 역할
ORM(Object Relational Mapper)은 관계형 데이터베이스의 테이블과 파이썬 클래스를 연결합니다. 클래스의 인스턴스는 보통 한 행에 대응하고 속성은 열에 대응합니다. 반복되는 SQL 문자열과 행 변환 코드를 줄일 수 있지만, 인덱스·조인·트랜잭션·N+1 조회 같은 데이터베이스 원리는 그대로 적용됩니다.
| 관계형 데이터베이스 | ORM 표현 | 예 |
|---|---|---|
| 테이블 | 매핑 클래스 | User |
| 행 | 인스턴스 | User(name="진") |
| 열 | 매핑 속성 | User.name |
| 기본키 | 식별 속성 | User.id |
| 외래키 | 열·관계 설정 | ForeignKey, relationship |
SQLAlchemy는 ORM뿐 아니라 SQL 표현식 중심의 Core도 제공합니다. 모든 조회를 객체로 가져올 필요가 없거나 복잡한 집계 SQL이 중심이라면 Core 표현이나 명시적 SQL을 함께 사용할 수 있습니다.
2. 설치하고 데이터베이스 엔진 연결하기
pip install SQLAlchemy
아래는 별도 서버 없이 연습할 수 있는 SQLite 파일 데이터베이스 연결입니다. echo=True를 사용하면 생성되는 SQL을 확인할 수 있어 학습과 디버깅에 유용하지만 운영에서는 로그량과 민감정보를 고려해야 합니다.
from sqlalchemy import create_engine
engine = create_engine(
"sqlite:///app.db",
echo=True,
)
PostgreSQL이나 MySQL은 해당 DBAPI 드라이버를 설치하고 맞는 연결 URL을 사용해야 합니다. 비밀번호가 포함된 URL은 코드나 저장소에 직접 적지 말고 환경변수 또는 비밀 저장소에서 읽어야 합니다.
3. DeclarativeBase로 테이블 매핑하기
SQLAlchemy 2.0의 형식 주석 기반 선언에서는 공통 베이스 클래스를 만들고, 매핑되는 속성에 Mapped[형식]을 표시합니다.
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
email: Mapped[str] = mapped_column(String(255), unique=True)
def __repr__(self) -> str:
return f"User(id={self.id!r}, name={self.name!r})"
__tablename__은 연결할 테이블 이름입니다. 기본키는 ORM이 객체를 식별하는 데 필수적입니다. Mapped[str | None]처럼 선택형으로 선언하면 널 허용 여부 추론에 영향을 줄 수 있으므로 파이썬 형식과 데이터베이스 제약조건이 같은 의도를 나타내도록 작성합니다.
__repr__은 디버깅을 돕지만 비밀번호, 토큰, 개인정보를 포함하지 않아야 합니다. 클래스 동작을 더 세밀하게 제어하는 방법은 매직 메서드 글에서 확인할 수 있습니다.
4. 메타데이터로 테이블 생성하기
Base.metadata.create_all(engine)
create_all()은 메타데이터에 등록된 테이블이 없을 때 생성하므로 예제와 초기 개발에 편리합니다. 하지만 이미 존재하는 열의 이름 변경, 자료형 변경, 데이터 이동 같은 운영 스키마 변경을 순서대로 관리하는 기능은 아닙니다. 운영 환경에서는 변경 이력을 코드로 관리하는 Alembic 같은 마이그레이션 도구를 검토해야 합니다.
SQLite 자체의 SQL과 트랜잭션을 먼저 이해하면 ORM이 생성한 쿼리를 해석하기 쉬워집니다.
5. Session으로 CRUD 실행하기
Session은 데이터베이스 대화와 ORM 객체 상태를 관리하는 작업 공간입니다. 컨텍스트 관리자를 사용하면 사용 범위를 명확히 할 수 있습니다.
추가하기
from sqlalchemy.orm import Session
with Session(engine) as session:
user = User(name="홍길동", email="hong@example.com")
session.add(user)
session.commit()
조회하기
from sqlalchemy import select
with Session(engine) as session:
stmt = select(User).where(User.name == "홍길동")
users = session.scalars(stmt).all()
수정하고 삭제하기
with Session(engine) as session:
user = session.get(User, 1)
if user is not None:
user.name = "김길동"
session.commit()
with Session(engine) as session:
user = session.get(User, 1)
if user is not None:
session.delete(user)
session.commit()
flush는 변경사항을 현재 트랜잭션 안에서 데이터베이스에 전달하지만 트랜잭션을 확정하지는 않습니다. commit이 성공해야 다른 트랜잭션에서도 확정된 변경으로 보입니다. 조회 결과에서 하나만 기대할 때는 scalar_one(), 없을 수도 있으면 scalar_one_or_none()처럼 기대 개수를 코드에 드러낼 수 있습니다.
6. ForeignKey와 relationship으로 관계 매핑하기
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
class Address(Base):
__tablename__ = "addresses"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
user: Mapped["User"] = relationship(back_populates="addresses")
User.addresses = relationship(
"Address",
back_populates="user",
cascade="all, delete-orphan",
)
ForeignKey는 데이터베이스 열의 참조 관계를 나타내고, relationship()은 ORM 객체 사이를 탐색하는 속성을 제공합니다. 삭제 cascade는 편리하지만 부모 삭제 시 어떤 자식 데이터가 실제로 삭제되는지 테스트해야 합니다. 관계를 반복 접근할 때 추가 SQL이 여러 번 실행되는 N+1 문제가 생길 수 있으므로 SQL 로그와 로딩 전략을 확인합니다.
7. 트랜잭션과 오류를 안전하게 처리하기
from sqlalchemy.exc import IntegrityError
with Session(engine) as session:
try:
session.add(User(name="중복", email="same@example.com"))
session.commit()
except IntegrityError:
session.rollback()
raise
무결성 제약 위반 등으로 flush 또는 commit이 실패하면 세션을 계속 사용하기 전에 rollback()이 필요합니다. 단순히 예외를 출력하고 무시하면 호출자는 저장 성공으로 오해할 수 있습니다. 트랜잭션 경계를 서비스 작업 단위와 맞추고, 여러 단계 중 일부만 저장되지 않도록 설계해야 합니다.
8. 실수 방지 체크리스트
- SQLAlchemy 1.x의
session.query()예제를 무심코 섞지 않았는지 확인합니다. - 모든 매핑 클래스에 안정적인 기본키가 있는지 확인합니다.
- 연결 URL과 SQL 로그에 비밀번호가 노출되지 않는지 점검합니다.
commit()실패 후rollback()을 수행하는지 확인합니다.- 세션을 전역으로 공유하거나 지나치게 오래 유지하지 않습니다.
create_all()을 운영 마이그레이션 대체재로 사용하지 않습니다.- 관계 접근에서 N+1 쿼리가 발생하지 않는지 SQL을 확인합니다.
- ORM 편의성만 보고 인덱스와 쿼리 실행 계획을 무시하지 않습니다.
자주 묻는 질문
1. SQLAlchemy는 데이터베이스인가요?
아닙니다. 파이썬 애플리케이션과 SQLite, PostgreSQL, MySQL 같은 데이터베이스 사이에서 SQL 구성과 객체 매핑을 돕는 라이브러리입니다.
2. ORM을 쓰면 SQL을 몰라도 되나요?
기본 CRUD는 편해지지만 성능, 조인, 인덱스, 트랜잭션 문제를 해결하려면 SQL 지식이 필요합니다.
3. SQLAlchemy 2.0의 조회 기본형은 무엇인가요?
select() 문을 만들고 Session.execute() 또는 Session.scalars()로 실행하는 방식이 기본 흐름입니다.
4. Mapped와 mapped_column은 왜 사용하나요?
파이썬 형식 주석과 ORM 열 매핑을 함께 명확히 표현해 도구와 사람이 모델 구조를 이해하기 쉽게 합니다.
5. Session은 데이터베이스 연결 하나인가요?
단순한 연결 그 자체라기보다 트랜잭션과 ORM 객체 상태를 관리하는 작업 단위입니다.
6. flush와 commit의 차이는 무엇인가요?
flush는 변경 SQL을 현재 트랜잭션에 반영하고, commit은 트랜잭션을 확정합니다.
7. create_all로 기존 열도 수정되나요?
운영 스키마의 일반적인 변경 이력을 관리하는 기능이 아닙니다. 기존 구조 변경에는 마이그레이션을 사용해야 합니다.
8. 객체를 조회했는데 추가 SQL이 계속 실행되는 이유는 무엇인가요?
관계 속성이 지연 로딩되면서 각 객체마다 쿼리가 실행되는 N+1 상황일 수 있습니다. 로딩 전략과 실제 SQL을 확인해야 합니다.
9. SQLite로 만든 코드를 PostgreSQL에서도 그대로 쓸 수 있나요?
공통 ORM 코드는 재사용할 수 있지만 자료형, 제약조건, 동시성, SQL 방언 차이가 있으므로 대상 DB에서 테스트해야 합니다.
10. 동기 Session과 AsyncSession 중 무엇을 선택하나요?
애플리케이션 실행 모델과 드라이버에 맞춰 선택합니다. 비동기 프레임워크라는 이유만으로 모든 DB 작업이 자동으로 빨라지는 것은 아닙니다.
공식 출처
- SQLAlchemy 2.0 ORM Quick Start
- SQLAlchemy 2.0 Database Metadata
- SQLAlchemy 2.0 Session Basics
- SQLAlchemy 2.0 Basic Relationship Patterns
공식 문서 확인일: 2026년 9월 2일