모델 정의 (Define Models)

모델 정의 (Define Models)

이제 코드로 들어가 볼게요. 👩‍💻

프로젝트 디렉터리 안에 있는지 먼저 확인해주세요.

우리가 할 일은 이렇습니다:

  • SQLModel 로 테이블을 정의하기
  • SQLModel 로 같은 SQLite 데이터베이스와 테이블을 만들기
  • DB Browser for SQLite 로 그 작업을 확인하기

원하는 테이블 구조를 다시 떠올려볼게요:

id name secret_name age
1 Deadpond Dive Wilson null
2 Spider-Boy Pedro Parqueador null
3 Rusty-Man Tommy Sharp 48

테이블 모델 클래스 만들기 (Create the Table Model Class)

가장 먼저 해야 할 일은 테이블 안의 데이터를 표현하는 클래스를 만드는 거예요.

이렇게 어떤 데이터를 표현하는 클래스를 흔히 모델(model) 이라고 불러요.

/// tip

그래서 이 패키지 이름이 SQLModel 인 거예요. 주로 SQL 모델을 만들 때 쓰이니까요.

///

그러려면 SQLModel 을 (그리고 앞으로 쓸 다른 것들도) 임포트하고, SQLModel 을 상속받아 우리 히어로들의 테이블 모델을 나타내는 클래스 Hero 를 만들면 됩니다:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None

이 클래스 Hero 는 우리 히어로들의 테이블을 나타내요. 그리고 앞으로 우리가 만드는 각 인스턴스는 테이블의 한 행을 나타내죠.

table=True 설정을 써서 SQLModel 에게 이건 테이블 모델이며 테이블을 나타낸다는 걸 알려줘요.

/// note

table=True 없이 만든 모델도 가능해요. 그런 모델은 데이터 모델일 뿐이고 데이터베이스에 테이블이 없으니 테이블 모델이 아니에요.

그런 데이터 모델들은 나중에 아주 유용해지는데, 지금은 일단 table=True 설정을 계속 붙여줄게요.

///

필드, 컬럼 정의하기 (Define the Fields, Columns)

다음 단계는 표준 파이썬 타입 어노테이션을 이용해 클래스의 필드(= 컬럼)를 정의하는 거예요.

이 변수들의 이름이 각각 테이블의 컬럼 이름이 되고,

각각의 타입이 테이블 컬럼의 타입이 됩니다:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None

이 필드/컬럼 선언들을 좀 더 자세히 살펴볼게요.

None 필드, Nullable 컬럼

age 부터 볼게요. 타입이 int | None 인 게 보이죠.

바로 이게 파이썬에서 어떤 값이 "int 이거나 None 일 수 있다"고 선언하는 표준 방식이에요.

그리고 age 의 기본값도 None 으로 설정해줬어요.

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None

/// tip

idint | None 으로 정의하긴 했는데, id 에 대한 이야기는 아래에서 할게요.

///

타입이 int | None 이기 때문에:

  • 데이터를 검증할 때 Noneage 의 허용 값이 돼요.
  • 데이터베이스에서 age 컬럼은 NULL (파이썬의 None 과 같은 SQL 값)을 허용하게 돼요.

그리고 = None 이라는 기본값이 있기 때문에:

  • 데이터를 검증할 때 이 age 필드는 필수(required)가 아니며, 기본적으로 None 이 되요.
  • 데이터베이스에 저장할 때 age 컬럼은 기본적으로 NULL 값을 가지게 돼요.

/// tip

기본값은 다른 걸로 할 수도 있어요. 예를 들어 = 42 처럼요.

///

기본 키 id

이제 id 필드를 살펴볼게요. 이건 테이블의 기본 키(primary key)예요.

그래서 id기본 키로 표시해야 해요.

그렇게 하려면 sqlmodel 의 특별한 함수 Field 를 쓰고 인자 primary_key=True 를 설정하면 돼요:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None

이렇게 해서 SQLModel 에게 이 id 필드/컬럼이 테이블의 기본 키라는 걸 알려줍니다.

그런데 SQL 데이터베이스 안에서는 id항상 필수이고 NULL 이 될 수 없어요. 그런데 왜 int | None 으로 선언해야 할까요?

id 는 데이터베이스에서 필수이긴 하지만, 우리 코드가 아니라 데이터베이스가 생성하는 값이에요.

그래서 (다음 장들에서) 이 클래스의 인스턴스를 만들 때마다 우리는 id설정하지 않을 거예요. 그리고 id 의 값은 데이터베이스에 저장하기 전까지 None 이다가, 저장된 뒤에야 비로소 값을 가지게 되는 거죠.

my_hero = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")

do_something(my_hero.id)  # Oh no! my_hero.id is None! 😱🚨

# Imagine this saves it to the database
somehow_save_in_db(my_hero)

do_something(my_hero.id)  # Now my_hero.id has a value generated in DB 🎉

즉, (데이터베이스가 아니라) 우리 코드 에서 id 값이 None 일 수 있기 때문에 int | None 을 쓰는 거예요. 이렇게 하면 편집기가 우리를 도와줄 수 있어요. 예를 들어 아직 데이터베이스에 저장하지 않아 여전히 None 인 객체의 id 에 접근하려 할 때 말이죠.

이제 우리가 Field() 함수로 기본값 자리를 대신했으니, Field() 의 인자 default=None 으로 id실제 기본값None 으로 설정해줍니다:

Field(default=None)

만약 default 값을 설정하지 않았다면, 나중에 이 모델로 데이터 검증을 할 때(Pydantic 으로 동작하죠) int 말고도 None 값을 허용하긴 하지만, 그 None 값을 꼭 전달하도록 요구하게 돼요. 그러면 이 모델을 나중에 쓰는 사람(아마 우리 자신이겠죠)이 혼란스러워져요. 그래서 여기서 기본값을 설정해 두는 게 낫습니다.

엔진 만들기 (Create the Engine)

이제 SQLAlchemy Engine 을 만들어야 해요.

엔진은 데이터베이스와의 통신을 처리하는 객체예요.

서버형 데이터베이스(예: PostgreSQL 이나 MySQL)를 쓴다면, 엔진이 그 데이터베이스로의 네트워크 연결을 들고 있게 돼요.

엔진을 만드는 건 아주 간단해요. 그냥 사용할 데이터베이스 URL을 넣어 create_engine() 을 호출하면 됩니다:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)

보통 애플리케이션 전체에 엔진 객체는 하나만 두고 모든 곳에서 그걸 재사용하는 게 좋아요.

/// tip

Session 이라는 관련된 다른 것도 있는데, 이건 보통 애플리케이션마다 하나만 두면 안 되는 객체예요.

그건 나중에 다룰게요.

///

엔진 데이터베이스 URL

지원되는 데이터베이스마다 자기만의 URL 형태가 있어요. 예를 들어 SQLite 는 파일 경로 뒤에 sqlite:/// 이 붙습니다. 예를 들면:

  • sqlite:///database.db
  • sqlite:///databases/local/application.db
  • sqlite:///db.sqlite

SQLite 는 전부 메모리 안에만 존재하는 특별한 데이터베이스를 지원해요. 그래서 아주 빠르지만, 프로그램이 끝나면 데이터베이스가 삭제되니 조심해야 해요. 슬래시 두 개(//)만 쓰고 파일 이름 없이 쓰면 이 인메모리 데이터베이스를 지정할 수 있어요:

  • sqlite://
from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)

SQLAlchemy (그리고 그 덕분에 SQLModel) 가 지원하는 모든 데이터베이스에 대해 훨씬 더 자세히 알고 싶다면 SQLAlchemy 문서를 읽어보세요.

엔진 에코 (Engine Echo)

이 예제에서는 인자 echo=True 도 함께 쓰고 있어요.

이러면 엔진이 실행하는 모든 SQL 문을 출력해줘서, 무슨 일이 벌어지는지 이해하는 데 도움을 줘요.

학습디버깅에 특히 유용합니다:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)

하지만 운영(production)에서는 아마 echo=True 를 빼고 싶을 거예요:

engine = create_engine(sqlite_url)

엔진 기술 세부사항

/// tip

SQLAlchemy 를 몰랐고 그냥 SQLModel 을 배우는 중이라면 이 섹션은 건너뛰고 아래로 내려가도 돼요.

///

엔진에 대해 더 자세히 알고 싶다면 SQLAlchemy 문서를 읽어보세요.

SQLModel 은 자체 create_engine() 함수를 정의해요. SQLAlchemy 의 create_engine() 과 같지만, 기본적으로 future=True 를 쓰도록 되어 있다는 차이가 있어요 (즉, 최신 SQLAlchemy인 1.4 스타일과 미래의 2.0 스타일을 쓴다는 뜻이죠).

그리고 SQLModel 의 create_engine() 은 내부적으로 타입 어노테이션이 되어 있어서, 편집기가 자동완성과 인라인 에러로 도와줄 수 있어요.

데이터베이스와 테이블 만들기

이제 드디어 데이터베이스와 테이블을 만들 준비가 모두 끝났습니다:

SQLModel.metadata.create_all(engine)

/// tip

엔진을 만드는 것만으로는 database.db 파일이 생기지 않아요.

하지만 SQLModel.metadata.create_all(engine) 을 실행하면 database.db 파일을 만들고 동시에 그 데이터베이스 안에 hero 테이블을 만들어줘요.

두 가지 모두 이 한 단계에서 처리되죠.

///

이걸 풀어서 볼게요:

SQLModel.metadata.create_all(engine)

SQLModel MetaData

SQLModel 클래스에는 metadata 속성이 있어요. 이건 MetaData 클래스의 인스턴스예요.

SQLModel 을 상속받고 table = True 로 설정된 클래스를 만들 때마다, 그 클래스는 이 metadata 속성에 등록됩니다.

그래서 마지막 줄까지 오면 SQLModel.metadata 에는 이미 Hero 가 등록되어 있어요.

create_all() 호출하기

SQLModel.metadata 에 있는 이 MetaData 객체는 create_all() 메서드를 가져요.

이 메서드는 엔진을 받아서, 그 엔진을 이용해 데이터베이스와 이 MetaData 객체에 등록된 모든 테이블을 만들어줍니다.

SQLModel MetaData 의 순서가 중요해요

이 말은 SQLModel.metadata.create_all()SQLModel 을 상속받는 새 모델 클래스를 만드는 코드 다음에 호출해야 한다는 뜻이기도 해요.

예를 들어 이런 상황을 상상해보죠:

  • 한 파이썬 파일 models.py 에서 모델들을 만든다.
  • db.py 파일에서 엔진 객체를 만든다.
  • app.py 에서 메인 앱을 만들고 SQLModel.metadata.create_all() 을 호출한다.

만약 app.py 에서 SQLModel 만 임포트한 채 SQLModel.metadata.create_all() 을 호출하려 하면, 테이블이 만들어지지 않아요:

# This wouldn't work! 🚨
from sqlmodel import SQLModel

from .db import engine

SQLModel.metadata.create_all(engine)

이건 SQLModel 을 혼자 임포트하면 파이썬이 (우리 예제의 Hero 같은) 그것을 상속받는 클래스를 만드는 모든 코드를 실행하지 않기 때문이에요. 그래서 SQLModel.metadata 는 여전히 비어 있죠.

하지만 SQLModel.metadata.create_all() 을 호출하기 전에 모델들을 임포트하면, 이건 동작해요:

from sqlmodel import SQLModel

from . import models
from .db import engine

SQLModel.metadata.create_all(engine)

모델들을 임포트하면 파이썬이 SQLModel 을 상속받는 클래스를 만드는 모든 코드를 실행하고, 그 클래스들을 SQLModel.metadata 에 등록하기 때문에 동작하죠.

대안으로, db.py 안에서 SQLModel 과 우리 모델들을 임포트할 수도 있어요:

# db.py
from sqlmodel import SQLModel, create_engine
from . import models


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url)

그리고 app.py 에서 db.py 로부터 SQLModel 을 임포트한 다음, 그곳에서 SQLModel.metadata.create_all() 을 호출하게 됩니다:

# app.py
from .db import engine, SQLModel

SQLModel.metadata.create_all(engine)

SQLModeldb.py 에서 임포트하면 동작하는 이유는, db.py 안에서도 SQLModel 이 임포트되기 때문이에요.

그리고 이 요령은 제대로 동작해서 데이터베이스에 테이블을 만들어줘요. 왜냐하면 db.py 에서 SQLModel 을 임포트하면 파이썬이 그 db.py 파일 안에서 SQLModel 을 상속받는 클래스(예: Hero)를 만드는 모든 코드를 실행하니까요.

마이그레이션

이 간단한 예제에서는, 그리고 Tutorial - User Guide 의 대부분에서는 SQLModel.metadata.create_all() 을 쓰는 걸로 충분해요.

하지만 운영 시스템이라면 아마 데이터베이스를 마이그레이션하는 시스템을 쓰는 게 좋을 거예요.

예를 들어 컬럼을 추가하거나 제거할 때, 새 테이블을 추가할 때, 타입을 바꿀 때 등에 그게 유용하고 중요해요.

마이그레이션에 대해서는 나중에 Advanced User Guide 에서 배우게 될 거예요.

프로그램 실행하기

프로그램을 실행해서 전부 제대로 동작하는지 확인해볼게요.

아직 안 했다면 코드를 app.py 파일에 넣어주세요.

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)

SQLModel.metadata.create_all(engine)

/// tip

프로젝트 환경 안에서 uv run 으로 실행해주세요.

///

이제 파이썬으로 프로그램을 실행해봅시다:

// We set echo=True, so this will show the SQL code
$ uv run python app.py

// First, some boilerplate SQL that we are not that interested in

INFO Engine BEGIN (implicit)
INFO Engine PRAGMA main.table_info("hero")
INFO Engine [raw sql] ()
INFO Engine PRAGMA temp.table_info("hero")
INFO Engine [raw sql] ()
INFO Engine

// Finally, the glorious SQL to create the table ✨

CREATE TABLE hero (
        id INTEGER,
        name VARCHAR NOT NULL,
        secret_name VARCHAR NOT NULL,
        age INTEGER,
        PRIMARY KEY (id)
)

// More SQL boilerplate

INFO Engine [no key 0.00020s] ()
INFO Engine COMMIT

/// note

위 출력을 읽기 쉽게 조금 단순화했어요.

실제로는 이렇게 보이는 대신:

INFO Engine BEGIN (implicit)

이런 느낌으로 보일 거예요:

2021-07-25 21:37:39,175 INFO sqlalchemy.engine.Engine BEGIN (implicit)

///

TEXT vs VARCHAR

이전 장의 예제에서는 일부 컬럼을 TEXT 로 테이블을 만들었어요.

그런데 이번 출력에서 SQLAlchemy 는 VARCHAR 를 쓰고 있죠. 무슨 일인지 알아볼게요.

각 SQL 데이터베이스는 지원하는 것에 차이가 있다는 걸 기억하시죠?

이게 그 차이 중 하나예요. 각 데이터베이스는 INTEGERTEXT 같은 특정 데이터 타입을 지원해요.

어떤 데이터베이스는 특정 용도에 특별한 타입을 갖고 있기도 해요. 예를 들어 PostgreSQL 과 MySQL 은 TrueFalse 값을 위한 BOOLEAN 을 지원해요. SQLite 는 테이블 컬럼을 정의할 때조차 boolean 을 포함한 SQL을 받아들이지만, 내부적으로 실제로 쓰는 건 INTEGER 예요. True1 로, False0 으로 나타내죠.

마찬가지로 문자열을 저장하는 타입도 여러 가지 가능해요. SQLite 는 TEXT 타입을 써요. 그런데 PostgreSQL 이나 MySQL 같은 다른 데이터베이스는 기본적으로 VARCHAR 타입을 쓰고, VARCHAR 는 가장 흔한 데이터 타입 중 하나죠.

VARCHARvariable(가변) length character(길이 문자)에서 온 거예요.

SQLAlchemy 는 VARCHAR 를 사용해 테이블을 만드는 SQL 문을 생성하고, SQLite 가 그걸 받아서 내부적으로 TEXT 로 변환해줍니다.

이 두 데이터 타입의 차이 말고도, MySQL 같은 일부 데이터베이스는 VARCHAR 타입에 최대 길이를 설정하도록 요구해요. 예를 들어 VARCHAR(255) 는 최대 문자 수를 255로 설정하죠.

SQLModel 을 어떤 데이터베이스를 쓰든 (MySQL 까지도) 추가 설정 없이 바로 시작하기 쉽게 하려고, 기본적으로 str 필드는 대부분의 데이터베이스에서 VARCHAR 로, MySQL 에서는 VARCHAR(255) 로 해석됩니다. 이렇게 하면 같은 클래스가 별다른 노력 없이 가장 흔한 데이터베이스들과 호환된다는 걸 알 수 있어요.

/// tip

문자열 컬럼의 최대 길이를 바꾸는 방법은 나중에 Advanced Tutorial - User Guide 에서 배우게 돼요.

///

데이터베이스 확인하기

이제 DB Browser for SQLite 로 데이터베이스를 열어보면, 프로그램이 이전과 똑같이 hero 테이블을 만들어낸 걸 볼 수 있어요. 🎉

데이터 생성 코드 리팩터링하기

이제 코드를 조금 재구성해서 나중에 재사용하고, 공유하고, 테스트하기 쉽게 만들어볼게요.

주요 **부수 효과(side effect)**가 있는 코드, 즉 데이터를 바꾸는(데이터베이스와 테이블이 있는 파일을 만드는) 코드를 함수로 옮겨봅시다.

이 예제에서는 그게 바로 SQLModel.metadata.create_all(engine) 예요.

그걸 create_db_and_tables() 라는 함수에 넣어볼게요:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)

만약 SQLModel.metadata.create_all(engine) 이 함수 안에 있지 않고, 다른 파일에서 이 모듈(이 파일)에서 무언가를 임포트하면, 그 다른 파일을 실행할 때마다 데이터베이스와 테이블을 만들려고 시도하게 돼요.

우리는 그렇게 되는 걸 원하지 않아요. 우리가 의도한 때에만 동작하길 원하죠. 그래서 함수 안에 넣는 거예요. 테이블이 만들어지는 건 다른 곳에서 이 모듈을 임포트할 때가 아니라, 그 함수를 호출할 때뿐임을 보장할 수 있으니까요.

이제 예를 들어 다른 파일에서 Hero 클래스를 임포트해도 그런 부수 효과 없이 할 수 있게 됐어요.

/// tip

😅 스포일러 경고: 함수 이름이 create_db_and_tables() 인 이유는, 앞으로 Hero 말고도 다른 클래스들로 이루어진 테이블이 더 생길 예정이기 때문이에요. 🚀

///

스크립트로 데이터 만들기

우리는 app.py 파일에서 뭔가를 임포트할 때 부수 효과가 생기지 않게 막았어요.

하지만 위에서 했던 것처럼 터미널에서 독립 스크립트로 파이썬으로 직접 호출할 때는 데이터베이스와 테이블을 만들도록 여전히 원해요.

/// tip

스크립트프로그램이라는 단어는 서로 바꿔 써도 된다고 생각하세요.

스크립트라는 단어는 보통 코드를 독립적이고 쉽게 실행할 수 있음을 암시해요. 또는 비교적 간단한 프로그램을 가리키기도 하죠.

///

그러려면 if 블록 안에서 특별한 변수 __name__ 을 쓰면 됩니다:

if __name__ == "__main__":
    create_db_and_tables()

__name__ == "__main__" 에 대해서

__name__ == "__main__" 의 주요 목적은, 이렇게 파일을 호출했을 때 실행되는 코드를 두는 거예요:

$ uv run python app.py

// Something happens here ✨

...그런데 다른 파일이 이걸 임포트했을 때는 실행되지 않도록 하는 거죠. 예를 들면:

from app import Hero

/// tip

if __name__ == "__main__": 을 쓰는 그 if 블록은 가끔 "main block" 이라고 불려요.

공식 이름(Python 문서)은 "Top-level script environment" 이에요.

///

더 자세히

파일 이름이 myapp.py 라고 해볼게요.

이렇게 실행하면:

$ uv run python myapp.py

// This will call create_db_and_tables()

...파이썬이 자동으로 만든, 파일 안의 내부 변수 __name__ 값이 문자열 "__main__" 이 돼요.

그래서 이 안의 함수는:

if __name__ == "__main__":
    create_db_and_tables()

...실행됩니다.


그런데 그 모듈(파일)을 임포트하면 이런 일이 벌어지지 않아요.

즉, importer.py 라는 다른 파일이 있다고 해보죠:

from myapp import Hero

# Some more code

...그 경우 myapp.py 안의 자동 변수 __name__"__main__" 이라는 값을 갖지 않아요.

그래서 이 줄은:

if __name__ == "__main__":
    create_db_and_tables()

...실행되지 않을 거예요.

/// note

더 자세한 내용은 공식 Python 문서를 확인해주세요.

///

마지막 점검 (Last Review)

이런 변경들 후에 다시 실행하면, 이전과 같은 출력이 만들어질 거예요.

하지만 이제 다른 파일에서 이 모듈의 것들을 임포트할 수 있게 됐죠.

이제 코드를 마지막으로 한 번 살펴볼게요:

from sqlmodel import Field, SQLModel, create_engine  # (2)!


class Hero(SQLModel, table=True):  # (3)!
    id: int | None = Field(default=None, primary_key=True)  # (4)!
    name: str  # (5)!
    secret_name: str  # (6)!
    age: int | None = None  # (7)!


sqlite_file_name = "database.db"  # (8)!
sqlite_url = f"sqlite:///{sqlite_file_name}"  # (9)!

engine = create_engine(sqlite_url, echo=True)  # (10)!


def create_db_and_tables():  # (11)!
    SQLModel.metadata.create_all(engine)  # (12)!


if __name__ == "__main__":  # (13)!
    create_db_and_tables()  # (14)!

/// tip

코드의 각 번호 표시를 클릭하면 그 줄이 하는 일을 확인할 수 있어요. 👆

///

정리 (Recap)

SQLModel 을 이용해 데이터베이스의 테이블이 어떤 모습이어야 하는지를 정의하는 법을 배웠고, SQLModel 로 데이터베이스와 테이블을 만들어봤어요.

나중에 재사용하고, 공유하고, 테스트하기 쉽도록 코드를 리팩터링하는 것도 했죠.

다음 장들에서는 SQLModel 이 코드에서 SQL 데이터베이스와 상호작용하는 걸 어떻게 도와주는지 볼 거예요. 🤓