SQLAlchemy 2.0 #1 전체 그림: Core와 ORM, 그리고 2.0 스타일

6 분 소요

파이썬으로 데이터베이스를 다루는 코드는 어디를 가도 결국 SQLAlchemy를 만납니다. FastAPI 프로젝트에서도, 데이터 파이프라인에서도, 오래된 Flask 앱에서도 그렇습니다. 문제는 인터넷에 1.x 시절 코드와 2.0 스타일 코드가 섞여 있어서, 검색으로 배우면 서로 호환되지 않는 문법이 뒤죽박죽이 된다는 점입니다. 이 시리즈는 2.0 스타일 하나로 통일해서 SQLAlchemy를 바닥부터 정리하는 7편입니다. 모던 파이썬 실전 #3에서 FastAPI 연동을 개요로 다뤘다면, 이 시리즈는 SQLAlchemy 자체를 깊게 들어갑니다.

SQLAlchemy는 두 층입니다: Core와 ORM #

SQLAlchemy를 처음 배울 때 가장 먼저 잡아야 할 그림은 이것입니다. SQLAlchemy는 하나의 라이브러리가 아니라 두 층의 스택입니다.

  • Core: 데이터베이스 연결(엔진, 커넥션 풀), 트랜잭션, 그리고 SQL을 파이썬 표현식으로 조립하는 SQL Expression Language까지. “SQL을 파이썬으로 쓰는 도구"입니다.
  • ORM: Core 위에 얹힌 층으로, 테이블의 행을 파이썬 객체로 매핑합니다. 객체를 만들고 수정하면 ORM이 변경을 추적해서 필요한 SQL을 대신 만들어 줍니다.

ORM을 쓰더라도 연결과 트랜잭션은 Core의 것을 그대로 쓰고, ORM 쿼리도 내부적으로는 Core 표현식으로 변환됩니다. 그래서 이 시리즈도 같은 순서로 갑니다. 다음 편에서 Core(엔진, 트랜잭션)를 먼저 다지고, 3편부터 ORM으로 올라갑니다. Core를 건너뛰고 ORM만 배우면 “커밋이 왜 안 됐지”, “커넥션이 왜 고갈됐지” 같은 문제에서 반드시 막힙니다.

어느 층을 주로 쓸지는 용도에 따라 갈립니다. 도메인 객체가 뚜렷한 애플리케이션(사용자, 주문, 게시글)은 ORM이 맞고, 대량 집계나 리포트처럼 행이 객체일 필요가 없는 작업은 Core가 단순하고 빠릅니다. 둘은 배타적이지 않아서 한 프로젝트에서 섞어 쓰는 것이 보통입니다.

1.x와 2.0: 무엇이 왜 바뀌었나 #

SQLAlchemy 2.0(2023년 릴리스)은 오랫동안 쌓인 두 가지 문제를 정리했습니다.

  1. 쿼리 방법이 여러 개였습니다. 1.x에서는 ORM은 session.query(User), Core는 select()로 쿼리 방식이 달랐습니다. 2.0은 select() 하나로 통일했습니다. 같은 문법이 Core에서도 ORM에서도 동작합니다.
  2. 암묵적 동작이 많았습니다. 자동 커밋, 암묵적 커넥션 같은 “알아서 해 주는” 동작이 편해 보이지만 디버깅을 어렵게 만들었습니다. 2.0은 트랜잭션 시작과 커밋을 코드에 드러내는 명시적 스타일로 바꿨습니다.

거기에 더해 2.0은 타입 힌트를 정식 지원합니다. 모델을 Mapped[int], Mapped[str]로 선언하면 IDE 자동 완성과 mypy 검사가 쿼리 결과까지 이어집니다. 이것만으로도 2.0 스타일로 넘어올 이유가 충분합니다.

실무에서 1.x 스타일 코드를 만났을 때 구분하는 요령은 간단합니다. session.query(...)가 보이면 1.x 스타일(2.0에서도 동작은 하지만 레거시), select(...)session.execute()session.scalars()에 넘기면 2.0 스타일입니다. 이 시리즈의 코드는 전부 후자입니다.

설치와 첫 연결 #

설치는 uv 기준으로 한 줄입니다. 패키지 관리가 처음이라면 파이썬 패키징 시리즈를 먼저 보시면 됩니다.

설치
uv add sqlalchemy

DB 서버 없이 시작하기 위해 SQLite를 씁니다. SQLite 드라이버는 파이썬에 내장되어 있어서 추가 설치가 없습니다. 연결의 시작점은 엔진(Engine)입니다.

main.py
from sqlalchemy import create_engine

engine = create_engine("sqlite:///app.db", echo=True)
  • 첫 인자는 연결 URL입니다. 방언+드라이버://사용자:비밀번호@호스트/DB이름 형식이고, SQLite는 파일 경로만 씁니다. PostgreSQL이라면 postgresql+psycopg2://user:pw@localhost/mydb처럼 됩니다.
  • echo=True는 실행되는 SQL을 전부 로그로 찍습니다. 학습 중에는 켜 두는 것을 권합니다. ORM이 내 코드를 어떤 SQL로 바꾸는지 눈으로 확인하는 것이 가장 빠른 학습법입니다.
  • create_engine은 이 시점에는 접속하지 않습니다. 엔진은 “연결하는 방법과 커넥션 풀"을 들고 있는 객체이고, 실제 접속은 첫 쿼리 때 일어납니다.

첫 쿼리: text()로 SQL 직접 실행 #

추상화를 배우기 전에, 가장 낮은 층부터 확인합니다. text()는 SQL 문자열을 그대로 실행하는 도구입니다.

main.py
from sqlalchemy import text

with engine.connect() as conn:
    result = conn.execute(text("SELECT 'hello' AS greeting"))
    print(result.all())  # [('hello',)]
  • engine.connect()는 풀에서 커넥션을 꺼내고, with 블록이 끝나면 반납합니다.
  • conn.execute()의 반환값은 Result 객체입니다. .all(), .first(), .scalar() 같은 메서드로 행을 꺼냅니다. 행은 튜플처럼도, 컬럼 이름으로도 접근할 수 있습니다.

파라미터는 반드시 바인딩으로 넘깁니다. 문자열 포매팅으로 SQL을 조립하는 것은 SQL 인젝션으로 가는 지름길입니다.

main.py
with engine.connect() as conn:
    result = conn.execute(
        text("SELECT :name AS name, :age AS age"),
        {"name": "김파이썬", "age": 30},
    )
    row = result.first()
    print(row.name, row.age)  # 김파이썬 30

text()만으로도 DB 작업은 전부 가능합니다. 그런데도 SQLAlchemy를 쓰는 이유는, 이 위의 층들이 SQL 문자열 조립의 오류를 컴파일 타임에 가깝게 앞당기고, DB 방언 차이를 흡수하고, 객체 매핑을 자동화해 주기 때문입니다. 시리즈를 따라가며 층을 하나씩 올라가겠습니다.

이 시리즈의 지도 #

주제핵심 질문
1 (이 글)전체 그림Core와 ORM은 무엇이고 2.0은 뭐가 다른가
2엔진과 트랜잭션커넥션 풀과 commit은 어떻게 동작하는가
3ORM 모델 정의테이블을 파이썬 클래스로 어떻게 선언하는가
4세션ORM은 변경을 어떻게 추적하고 언제 SQL을 날리는가
5관계 매핑1:N, N:M 관계와 N+1 문제를 어떻게 다루는가
6쿼리 심화조인, 집계, 서브쿼리, 대량 처리
7Alembic과 실전 구성스키마 변경을 어떻게 관리하고 비동기는 어떻게 하는가

정리 #

  • SQLAlchemy는 Core(연결, 트랜잭션, SQL 표현식)와 ORM(객체 매핑)의 2층 구조입니다. ORM도 Core 위에서 돕니다.
  • 2.0 스타일은 select()로 쿼리를 통일하고, 트랜잭션을 명시적으로 만들고, 타입 힌트를 정식 지원합니다. session.query()가 보이면 레거시 스타일입니다.
  • 연결의 시작점은 create_engine()이고, 엔진은 연결 방법과 풀을 관리할 뿐 생성 시점에 접속하지 않습니다.
  • text()와 파라미터 바인딩으로 SQL을 직접 실행할 수 있습니다. 문자열 포매팅으로 SQL을 조립하면 안 됩니다.
  • 다음 편에서는 엔진과 커넥션 풀, 그리고 2.0의 명시적 트랜잭션 패턴 두 가지를 다룹니다.
X