본문 바로가기

SQLite와 SQLAlchemy

1. SQLite와 SQLAlchemy

실제 애플리케이션에서는 데이터를 영구적으로 저장하고 사용하기 위해 데이터베이스를 사용합니다. 앞서 우리는 리스트나 딕셔너리로 데이터를 관리했습니다. 서버를 다시 시작할 때마다 데이터가 사라지는 것을 여러 번 경험하셨을 겁니다. 이 챕터에서는 간단하게 사용할 수 있는 SQLite를 SQLAlchemy를 통하여 연동하는 방법을 알아보겠습니다. SQLite는 파일 기반의 경량 데이터베이스이며, SQL이라는 언어를 통해 데이터를 저장하고 조회할 수 있습니다. 별도의 서버를 설치하고 실행할 필요가 없어 학습과 소규모 프로젝트에 적합하고, 파이썬에 기본으로 포함되어 있어 설치도 필요 없습니다. 아래 서비스를 통해 SQL 언어를 간단하게 실습해볼 수 있습니다.

weniv SQL

SQL언어는 데이터베이스를 다룬다면 필수로 알아야 하는 언어입니다. PostgreSQL, MySQL 등의 데이터베이스를 사용하더라도 마찬가지입니다.

그런데 SQLAlchemy를 사용하면 데이터베이스를 파이썬 코드로 쉽게 다룰 수 있게 해줍니다. 이렇게 프로그래밍 언어로 데이터베이스를 다루는 것을 ORM(Object-Relational Mapping)이라고 합니다. 이렇게 데이터베이스를 파이썬 코드로 다루는 것이 편리하기 때문에 많은 프레임워크에서 사용합니다. 그렇다 하더라도 프로그래머에게 데이터베이스를 다루는 것은 필수이므로 SQL을 꼭 공부해야 합니다. 우리 수업에서는 SQL 구문까지 다루지는 않습니다. 더 자세한 내용은 아래 무료 강의를 참고해주세요.

SQL 베이스캠프

앞서 말한 SQLite와 SQLAlchemy는 모두 FastAPI와 독립적입니다. FastAPI가 아니더라도 이 라이브러리를 사용하는 곳은 많습니다. 그렇기에 이 라이브러리들을 좀 더 쉽게 이해하기 위해 FastAPI와 독립적으로 살펴보도록 하겠습니다. 복잡도를 낮출 수 있기 때문이죠. 한 번에 여러 개를 배우면 어디에서 문제가 생겼는지 알기 어렵습니다. 추후 이 내용들을 FastAPI와 연동하는 방법을 알아보겠습니다.

1.1 실습 환경 준비

이번 절의 코드는 Colab에서도 실행할 수 있고, 로컬 폴더에서 실행해도 됩니다. 로컬에서 진행한다면 아래와 같이 준비합니다.

앞 실습의 서버가 실행 중이면 Ctrl + C로 멈추고, 가상환경이 켜져 있다면 deactivate로 빠져나옵니다. 아래 명령은 실습 폴더들을 모아둔 상위 폴더에서 실행하세요.

mkdir 04_1_orm
cd 04_1_orm
python -m venv venv
.\venv\Scripts\Activate.ps1
pip install sqlalchemy

macOS/Linux에서는 python -m venv venv 대신 python3 -m venv venv를, 활성화 명령 대신 source ./venv/bin/activate를 사용합니다. 이후 명령은 가상환경이 활성화된 상태에서 실행합니다. Colab에서는 sqlalchemy가 이미 설치되어 있으므로 코드 셀에 그대로 붙여넣어 실행하면 됩니다.

2. SQLite Colab 실습

간단하게 Colab에서 실습할 수 있는 코드를 준비했습니다. 이 코드로 Python이 어떻게 SQLite를 사용하여 데이터를 저장하고 조회하는지 확인할 수 있습니다. 먼저 ORM 없이 SQL을 직접 써봅니다. ORM이 무엇을 대신해주는지 알아야 ORM의 코드가 이해되기 때문입니다. 여기서 SQL 구문이나 sqlite3 라이브러리를 상세하게 이해하실 필요는 없습니다. 어떤 식으로 사용하는지 이해하는 것이 중요합니다.

로컬에서 진행한다면 step1_sqlite.py 파일을 만들고 아래 코드를 넣습니다.

import sqlite3

# 데이터베이스 파일에 연결합니다. 파일이 없으면 새로 만듭니다.
conn = sqlite3.connect("example.db")
# 커서는 지금 깜빡이고 있는 커서라고 생각하시면 됩니다.
cursor = conn.cursor()

# 테이블 생성
# users 테이블이 없으면 만들고
# id는 자동으로 증가하는 숫자값
# name은 비울 수 없는 값
# age는 숫자값
cursor.execute(
    """
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        age INTEGER
    )
    """
)

# 데이터 삽입
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("홍길동", 20))
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("김철수", 30))
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("김숙희", 30))
conn.commit()

# 삽입된 데이터 확인
# execute는 SQL 구문을 실행할 뿐 결과를 보여주지 않습니다.
cursor.execute("SELECT * FROM users")
# 결과를 보려면 fetch를 해야 합니다.
# 전부 가져오는 것은 fetchall, 하나만 가져오는 것은 fetchone입니다.
print(cursor.fetchall())

# 데이터 수정
cursor.execute("UPDATE users SET age = ? WHERE name = ?", (25, "홍길동"))
conn.commit()

# 데이터 삭제
cursor.execute("DELETE FROM users WHERE name = ?", ("김철수",))
conn.commit()

# 수정된 데이터 확인
cursor.execute("SELECT * FROM users")
print(cursor.fetchall())

conn.close()

로컬에서는 아래 명령으로 실행합니다.

python step1_sqlite.py

여기서 commit()은 데이터베이스에 변경사항을 저장하는 것입니다. 이 코드를 실행하지 않으면 데이터베이스에 변경사항이 저장되지 않습니다. 문서를 편집하고 저장 버튼을 누르는 것과 비슷하다고 생각하시면 됩니다.

값을 넣을 때 ?를 쓰는 이유

아래처럼 문자열을 이어붙여서 SQL을 만들 수도 있습니다.

# 이렇게 쓰면 안 됩니다
name = "홍길동"
cursor.execute(f"INSERT INTO users (name, age) VALUES ('{name}', 20)")

이렇게 만들면 name에 SQL 구문이 들어왔을 때 그것이 그대로 실행됩니다. 이것을 SQL 인젝션이라고 하며, 웹 서비스에서 가장 오래되고 가장 자주 발생하는 보안 사고입니다. 값을 ?로 비워두고 튜플로 따로 전달하면 라이브러리가 알아서 안전하게 처리해 줍니다.

ORM을 쓰면 이 처리가 기본으로 이루어집니다. 이것도 ORM을 쓰는 이유 중 하나입니다.

3. SQLAlchemy colab 실습

이번에는 SQLAlchemy를 사용하여 데이터베이스를 다루는 방법을 알아보겠습니다. 앞서 소개한 것과 같이 SQLAlchemy는 데이터베이스를 파이썬 코드로 쉽게 다룰 수 있게 해줍니다. 이렇게 프로그래밍 언어로 데이터베이스를 다루는 것을 ORM(Object-Relational Mapping)이라고 합니다. SQLAlchemy는 sqlite3 뿐만 아니라 MySQL, PostgreSQL, Oracle 등의 데이터베이스를 지원합니다. 여기서는 sqlite3를 사용하여 실습해보겠습니다. 위에서 SQL로 적었던 것을 파이썬 코드로 바꾼다고 생각하시면 됩니다.

로컬에서 진행한다면 step2_orm.py 파일을 만들고 아래 코드를 넣습니다.

from sqlalchemy import String, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column

# 데이터베이스 연결 설정
# 'sqlite:///sample.db'와 'sqlite:///./sample.db'는 같습니다.
# echo=True로 두면 실행되는 SQL문이 터미널에 출력됩니다. 공부할 때 좋습니다.
engine = create_engine("sqlite:///sample.db", echo=True)


# 모든 모델의 부모가 되는 클래스입니다.
class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "users"  # 테이블 이름

    id: Mapped[int] = mapped_column(primary_key=True)  # 기본키
    name: Mapped[str] = mapped_column(String(50))  # 이름 컬럼, 50자 제한
    age: Mapped[int | None]  # 나이 컬럼, 비어 있어도 됨

    def __repr__(self) -> str:
        return f"<User {self.id}, {self.name}, {self.age}>"


# 테이블 생성
Base.metadata.create_all(engine)

# 세션은 데이터베이스와 대화하는 창구입니다.
# with 블록을 벗어나면 자동으로 닫힙니다.
with Session(engine) as session:
    # 데이터 추가
    session.add(User(name="licat", age=10))
    session.add(User(name="mura", age=20))
    session.commit()

    # 전체 조회
    users = session.scalars(select(User)).all()
    print("전체:", users)

    # 조건 조회
    mura = session.scalars(select(User).where(User.name == "mura")).first()
    print("mura:", mura)

    # 수정
    if mura:
        mura.age = 100
        session.commit()
    print("수정 후:", session.scalars(select(User)).all())

    # 삭제
    if mura:
        session.delete(mura)
        session.commit()
    print("삭제 후:", session.scalars(select(User)).all())

로컬에서는 아래 명령으로 실행합니다.

python step2_orm.py

echo=True 덕분에 터미널에 실제로 실행된 SQL이 출력됩니다. 파이썬으로 적은 select(User).where(User.name == "mura")가 아래처럼 SQL로 바뀌는 것을 볼 수 있습니다.

SELECT users.id, users.name, users.age
FROM users
WHERE users.name = ?

ORM은 이렇게 파이썬 코드를 SQL로 번역해주는 역할을 합니다. 결국 데이터베이스로 나가는 것은 SQL이며, ORM이 만들어 준 SQL이 이상하면 성능 문제가 생깁니다. SQL을 알아야 하는 이유가 여기에 있습니다.

3.1 코드 한 줄씩 보기

코드하는 일
create_engine(...)어떤 데이터베이스에 어떻게 연결할지 정의합니다
class Base(DeclarativeBase)모델들의 공통 부모입니다. 한 번만 만듭니다
__tablename__이 클래스가 어떤 테이블에 대응하는지 지정합니다
Mapped[int]이 필드가 정수 컬럼이라는 뜻입니다
mapped_column(...)기본키, 길이 제한 같은 세부 설정을 붙입니다
Base.metadata.create_all(engine)정의한 클래스대로 실제 테이블을 만듭니다
Session(engine)데이터베이스와 대화하는 창구를 엽니다
session.add(...)추가할 것을 세션에 올려둡니다. 아직 저장되지 않았습니다
session.commit()세션에 올려둔 변경사항을 실제로 저장합니다
session.scalars(select(...))조회 결과를 객체 형태로 가져옵니다

Mapped[int]와 Mapped[int | None]의 차이를 보세요. | None이 붙으면 데이터베이스에서 NULL을 허용합니다. 파이썬의 타입 힌트가 그대로 테이블 정의가 되는 구조입니다. 2장에서 배운 "타입 힌트가 곧 규칙"이라는 성질이 여기서도 이어집니다.

3.2 오래된 SQLAlchemy 코드 알아보기

SQLAlchemy는 2.0에서 크게 바뀌었습니다. 인터넷 자료와 AI가 만들어 주는 코드에는 아직 1.x 방식이 많습니다. 아래 왼쪽이 보이면 오래된 코드입니다.

1.x (오래된 방식)2.0 (지금 방식)
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Base(DeclarativeBase): pass
name = Column(String(50))name: Mapped[str] = mapped_column(String(50))
session.query(User).all()session.scalars(select(User)).all()
session.query(User).filter(...).first()session.scalars(select(User).where(...)).first()
Session = sessionmaker(bind=engine)
session = Session()
with Session(engine) as session:

특히 첫 줄의 from sqlalchemy.ext.declarative import declarative_base는 실행하면 경고가 출력됩니다. 이 경고가 보이면 오래된 코드를 그대로 복사했다는 신호입니다.

1.x 방식도 아직 동작하지만, 2.0 방식은 타입 힌트를 쓰기 때문에 에디터가 오타와 타입 오류를 미리 잡아준다는 장점이 있습니다. session.query(User).filter(User.nmae == "x") 같은 오타를 실행 전에 발견할 수 있습니다.

4. 만들어진 데이터베이스 파일 확인하기

로컬에서 실행했다면 sample.db 파일이 폴더에 생성되었을 겁니다. 이 파일을 더블클릭해도 열리지 않습니다. 1장에서 설치한 SQLite Viewer 익스텐션이 있다면 VS Code에서 파일을 클릭하는 것만으로 표 형태의 내용을 볼 수 있습니다.

익스텐션 대신 DB Browser for SQLite 같은 별도 프로그램을 쓰셔도 됩니다.

.db 파일은 Git에 올리지 마세요

데이터베이스 파일에는 실제 데이터가 들어 있습니다. 실습용이라 상관없어 보이지만, 실무에서는 이 파일이 개인정보 유출 사고의 원인이 되기도 합니다. .gitignore에 아래 줄을 추가하는 습관을 들이세요.

*.db
*.sqlite3

연습문제

  1. User 모델에 email 컬럼을 추가하고, 같은 이메일이 두 번 저장되지 않도록 만들어보세요. 힌트로 mapped_column(unique=True)를 사용합니다.
  2. 나이가 20 이상인 사용자만 조회하는 코드를 작성해보세요. 힌트로 select(User).where(User.age >= 20)을 사용합니다.
  3. echo=True를 켜둔 상태에서 2번을 실행하고, 출력된 SQL을 읽어보세요.
  4. 사용자를 이름순으로 정렬해서 가져오는 코드를 작성해보세요. 힌트로 .order_by(User.name)을 사용합니다.
SQLite와 SQLAlchemy - FastAPI 베이스캠프 | 위니버시티