데이터베이스 연결 (Database)
데이터베이스 연결 (Database)
출처: SQLModel 공식 튜토리얼 — "Create a Table with SQLModel - Use the Engine" https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/
이제 본격적으로 코드를 볼 차례예요. 👩💻
시작하기 전에, 프로젝트 디렉터리 안에 있는지 먼저 확인해 주세요.
이번 장에서 우리가 할 일을 정리하면 이렇게 돼요:
- SQLModel로 테이블 하나를 정의하고
- 같은 SQLite 데이터베이스와 테이블을 SQLModel로 만들고
- 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 |
테이블 모델 클래스 만들기
가장 먼저 해야 할 일은, 테이블에 담길 데이터를 표현하는 클래스를 하나 만드는 거예요.
이렇게 어떤 데이터를 표현하는 클래스를 보통 **모델(model)**이라고 불러요.
팁
그래서 이 패키지 이름이
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
# 아래 코드는 생략했어요 👇
👀 전체 파일 미리보기
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)
이 Hero 클래스가 우리 히어로들의 테이블을 나타내는 거예요. 그리고 나중에 만들 각 인스턴스 하나하나는 그 테이블의 행(row, 레코드) 하나를 나타내게 돼요.
table=True 설정을 넣은 건, 이 클래스가 테이블 모델이라서 데이터베이스의 테이블을 나타낸다는 걸 SQLModel에 알려주기 위해서예요.
참고
table=True없이 쓰는 모델도 가능해요. 그런 모델은 **데이터 모델(data model)**만 되고, 데이터베이스에 실제 테이블은 생기지 않아요. 테이블 모델이 아닌 거죠.그런 데이터 모델은 나중에 정말 유용하게 쓰이지만, 지금은 우선
table=True설정만 계속 붙인다고 생각해 주세요.
필드(Field), 컬럼(Column) 정의하기
다음 단계는 표준 파이썬 타입 어노테이션(type annotation)을 써서 클래스의 필드, 그러니까 컬럼을 정의하는 일이에요.
각 변수의 이름이 테이블의 컬럼 이름이 되고,
각 변수의 타입이 테이블 컬럼의 타입이 되는 거예요:
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
# 아래 코드는 생략했어요 👇
👀 전체 파일 미리보기
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)
이제 이 필드·컬럼 선언들을 좀 더 자세히 살펴볼게요.
None 필드, NULL 가능 컬럼
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
# 아래 코드는 생략했어요 👇
👀 전체 파일 미리보기
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)
팁
id도int | None으로 선언하는데, 그 이유는 아래에서 다룰게요.
타입이 int | None이기 때문에 생기는 일은 이래요:
- 데이터 검증(validation)할 때,
age에는None이 허용되는 값이 돼요. - 데이터베이스에서는
age컬럼에NULL(파이썬의None에 해당하는 SQL 값)이 들어갈 수 있어요.
그리고 기본값이 = None이기 때문에:
- 데이터 검증할 때,
age필드는 필수 값이 아니고, 기본적으로None이 돼요. - 데이터베이스에 저장할 때,
age컬럼은 기본값으로NULL값을 갖게 돼요.
팁
기본값은 꼭
None일 필요는 없어요. 예를 들어= 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
# 아래 코드는 생략했어요 👇
👀 전체 파일 미리보기
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)
이렇게 해서 id라는 필드·컬럼이 테이블의 기본 키라는 걸 SQLModel에 알려주는 거예요.
그런데 잠깐, SQL 데이터베이스 안에서 기본 키는 항상 필수이고 NULL이 될 수 없잖아요. 그런데 왜 int | None으로 선언하는 걸까요?
id는 데이터베이스에서 항상 필요하긴 하지만, 값은 우리 코드가 아니라 데이터베이스가 생성하게 돼 있어요.
그래서 이 클래스의 인스턴스를 만들 때(다음 장들에서 만들 거예요) id는 설정하지 않아요. id의 값은 데이터베이스에 저장하기 전까지는 None이고, 저장하고 나서야 비로소 값을 갖게 돼요.
my_hero = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
do_something(my_hero.id) # 이런! my_hero.id 는 아직 None 이에요! 😱🚨
# 이것이 데이터베이스에 저장한다고 상상해 봐요
somehow_save_in_db(my_hero)
do_something(my_hero.id) # 이제 my_hero.id 는 DB에서 생성된 값을 갖게 됐어요 🎉
그래서 데이터베이스가 아닌 우리 코드에서는 id 값이 None일 수도 있으니, int | None으로 표기하는 거예요. 이렇게 하면 에디터가 도움을 줄 수 있어요. 예를 들어 아직 데이터베이스에 저장하지 않은 객체의 id에 접근하려다가(그 값이 여전히 None일 텐데) 에디터가 미리 경고해 줄 수 있거든요.

그리고 우리가 Field() 함수로 기본값 자리를 채우고 있으니까, Field() 안에서 default=None 인자로 id의 실제 기본값을 None으로 설정해 줘요:
Field(default=None)
만약 default 값을 설정하지 않으면, 나중에 이 모델로 데이터 검증(Pydantic 기반)을 할 때 None 값을 받아들이긴 하지만, 여전히 그 None 값을 꼭 전달하도록 요구하게 돼요. 그러면 나중에 이 모델을 쓰는 사람(아마 우리 자신일 거예요)이 헷갈리게 되니까, 여기서 기본값을 설정해 두는 게 좋아요.
엔진(Engine) 만들기
이제 SQLAlchemy Engine을 만들 차례예요.
엔진은 데이터베이스와의 통신을 처리하는 객체예요.
만약 서버형 데이터베이스(예: PostgreSQL이나 MySQL)를 쓴다면, 엔진이 그 데이터베이스와의 **네트워크 연결(network connections)**을 관리하게 돼요.
엔진을 만드는 건 아주 간단해요. 사용할 데이터베이스의 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)
SQLModel.metadata.create_all(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)
SQLModel.metadata.create_all(engine)
보통은 애플리케이션 전체에서 엔진 객체 하나를 만들고, 어디서든 그걸 재사용하는 게 정석이에요.
팁
여기에 관련된 또 다른 개념으로 **세션(Session)**이 있는데, 이건 보통 애플리케이션마다 객체 하나를 쓰면 안 돼요.
하지만 그것에 대해서는 나중에 다룰게요.
엔진 데이터베이스 URL
지원되는 데이터베이스마다 자기 고유의 URL 형태가 있어요. 예를 들어 SQLite는 sqlite:/// 다음에 파일 경로가 오는 형태예요. 몇 가지 예를 들면:
sqlite:///database.dbsqlite:///databases/local/application.dbsqlite:///db.sqlite
SQLite는 메모리(in memory)에만 존재하는 특별한 데이터베이스도 지원해요. 그래서 아주 빠르지만, 프로그램이 끝나면 데이터베이스가 사라지니 조심해야 해요. 이 인메모리 데이터베이스는 슬래시 두 개(//)만 쓰고 파일 이름 없이 지정할 수 있어요:
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)
SQLModel.metadata.create_all(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)
SQLModel.metadata.create_all(engine)
SQLAlchemy(그리고 그렇게 해서 SQLModel)가 지원하는 모든 데이터베이스에 대해 더 자세히 알고 싶다면 SQLAlchemy 문서를 확인해 보세요.
엔진 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)
SQLModel.metadata.create_all(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)
SQLModel.metadata.create_all(engine)
하지만 실제 운영(프로덕션) 환경에서는 echo=True를 빼는 게 좋을 거예요:
engine = create_engine(sqlite_url)
엔진 기술적 세부사항
팁
SQLAlchemy를 전혀 몰랐고 지금 SQLModel을 처음 배우는 중이라면, 이 절은 건너뛰고 아래로 내려가도 돼요.
엔진에 대해 더 자세히 알고 싶다면 SQLAlchemy 문서를 참고하세요.
SQLModel은 자기만의 create_engine() 함수를 정의해요. SQLAlchemy의 create_engine()과 같지만, 기본적으로 future=True를 쓰는 차이가 있어요(즉 최신 SQLAlchemy 스타일, 1.4 그리고 미래의 2.0 스타일을 쓴다는 뜻이에요).
그리고 SQLModel의 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)
SQLModel.metadata.create_all(engine)
팁
엔진을 만든다고 해서
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을 상속하는 새 모델 클래스를 만드는 코드 다음에 SQLModel.metadata.create_all()을 호출해야 한다는 뜻이기도 해요.
예를 들어 이런 상황을 상상해 볼게요:
- 모델들을 한 파이썬 파일
models.py에 만든다. - 엔진 객체는 다른 파일
db.py에 만든다. - 메인 앱을 만들고
app.py에서SQLModel.metadata.create_all()을 호출한다.
만약 SQLModel만 임포트하고 app.py에서 SQLModel.metadata.create_all()을 호출하려 하면, 테이블이 만들어지지 않아요:
# 이건 작동하지 않아요! 🚨
from sqlmodel import SQLModel
from .db import engine
SQLModel.metadata.create_all(engine)
이게 안 되는 이유는, SQLModel만 임포트하면 파이썬이 (우리 예시의 Hero 같은) SQLModel을 상속하는 클래스를 만드는 코드를 실행하지 않기 때문이에요. 그래서 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에 등록하기 때문에 작동하는 거예요.
대안으로, SQLModel과 모델들을 db.py 안에서 임포트해도 됩니다:
# 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에서 SQLModel을 db.py로부터 임포트해서, 거기서 SQLModel.metadata.create_all()을 호출하면 돼요:
# app.py
from .db import engine, SQLModel
SQLModel.metadata.create_all(engine)
SQLModel이 db.py에서도 임포트되기 때문에, db.py로부터 SQLModel을 임포트하는 건 잘 작동해요.
그리고 이 트릭은 정확히 작동해서 데이터베이스에 테이블을 만들어 줘요. db.py에서 SQLModel을 임포트하면 파이썬이 그 db.py 파일 안에서 SQLModel을 상속하는 클래스들(예를 들어 Hero)을 만드는 코드를 모두 실행하기 때문이에요.
마이그레이션(Migrations)
이번처럼 간단한 예시에서는, 그리고 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)
팁
프로젝트 환경 안에서
uv run으로 실행해 주세요.
이제 파이썬으로 프로그램을 실행해 볼게요:
// echo=True를 설정했으니 SQL 코드가 보일 거예요
$ uv run python app.py
// 먼저, 우리가 별로 관심 없을 보일러플레이트 SQL이 나와요
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
// 마침내, 테이블을 만드는 장엄한 SQL이 나와요 ✨
CREATE TABLE hero (
id INTEGER,
name VARCHAR NOT NULL,
secret_name VARCHAR NOT NULL,
age INTEGER,
PRIMARY KEY (id)
)
// SQL 보일러플레이트가 좀 더 이어져요
INFO Engine [no key 0.00020s] ()
INFO Engine COMMIT
참고
위 출력은 읽기 쉽게 하려고 약간 단순화했어요.
실제로는
INFO Engine BEGIN (implicit)대신 이런 식으로 보일 거예요:
2021-07-25 21:37:39,175 INFO sqlalchemy.engine.Engine BEGIN (implicit)
TEXT 또는 VARCHAR
이전 장의 예시에서는 몇몇 컬럼에 TEXT를 써서 테이블을 만들었어요.
하지만 이번 출력에서는 SQLAlchemy가 VARCHAR를 쓰고 있네요. 무슨 일인지 살펴볼게요.
각 SQL 데이터베이스마다 지원하는 것에 조금씩 차이가 있다는 걸 기억하나요?
이게 바로 그 차이 중 하나예요. 각 데이터베이스는 INTEGER나 TEXT 같은 특정 데이터 타입을 지원해요.
어떤 데이터베이스는 특정 용도의 특별한 타입이 있기도 해요. 예를 들어 PostgreSQL과 MySQL은 True와 False 값을 위한 BOOLEAN을 지원해요. SQLite는 테이블 컬럼을 정의할 때도 불리언이 포함된 SQL은 받아들이지만, 실제 내부적으로는 INTEGER를 쓰고, True는 1, False는 0으로 표현해요.
마찬가지로 문자열을 저장하는 타입도 여러 가지가 가능해요. SQLite는 TEXT 타입을 써요. 하지만 PostgreSQL이나 MySQL 같은 다른 데이터베이스는 기본적으로 VARCHAR 타입을 쓰는데, VARCHAR는 가장 흔한 데이터 타입 중 하나예요.
**VARCHAR**는 variable(가변) character(문자)에서 온 말이에요.
SQLAlchemy는 테이블을 만드는 SQL 문을 VARCHAR로 생성하고, 그러면 SQLite가 그것을 받아서 내부적으로 TEXT로 변환해요.
이 두 데이터 타입의 차이 말고도, MySQL 같은 일부 데이터베이스는 VARCHAR 타입에 최대 길이를 설정해야 해요. 예를 들어 VARCHAR(255)는 최대 문자 수를 255로 설정하는 거예요.
SQLModel을 어떤 데이터베이스에서든 (MySQL에서도) 추가 설정 없이 바로 쓰기 쉽게 만들기 위해, 기본적으로 str 필드는 대부분의 데이터베이스에서 VARCHAR로, MySQL에서는 VARCHAR(255)로 해석돼요. 이렇게 해서 같은 클래스가 별다른 노력 없이 가장 인기 있는 데이터베이스들과 호환된다는 걸 알 수 있어요.
팁
문자열 컬럼의 최대 길이를 바꾸는 방법은 나중에 Advanced Tutorial - User Guide에서 배우게 돼요.
데이터베이스 확인하기
이제 DB Browser for SQLite로 데이터베이스를 열어 보면, 프로그램이 저번과 똑같이 hero 테이블을 만든 걸 볼 수 있을 거예요. 🎉

데이터 생성 코드 리팩터링하기
이제 나중에 재사용하고, 공유하고, 테스트하기 쉽도록 코드를 조금 재구성할게요.
데이터를 바꾸는(데이터베이스와 테이블이 있는 파일을 만드는) 주요 **부수 효과(side effects)**가 있는 코드를 함수로 옮길 거예요.
이 예시에서는 그것이 바로 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)
# 아래 코드는 생략했어요 👇
👀 전체 파일 미리보기
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)
if __name__ == "__main__":
create_db_and_tables()
만약 SQLModel.metadata.create_all(engine)이 함수 안에 없고, 다른 파일에서 이 모듈(이 파일)에서 뭔가를 임포트한다면, 그 다른 파일을 실행할 때마다 데이터베이스와 테이블을 만들려고 할 거예요.
우리는 그렇게 되는 걸 원하지 않아요. 우리가 의도한 때에만 일어나길 바라잖아요. 그래서 함수에 넣는 거예요. 테이블이 이 함수를 호출할 때만 만들어지고, 이 모듈이 다른 곳에서 임포트될 때는 만들어지지 않게 할 수 있으니까요.
이제 예를 들어 다른 파일에서 Hero 클래스를 임포트해도 그런 부수 효과 없이 쓸 수 있게 됐어요.
팁
😅 스포일러 주의: 함수 이름이
create_db_and_tables()인 이유는, 앞으로Hero말고도 다른 클래스로 테이블이 더 생길 거라서예요. 🚀
스크립트로 데이터 생성하기
app.py 파일에서 뭔가를 임포트할 때 부수 효과가 발생하는 건 막았어요.
하지만 위에서처럼 터미널에서 독립된 스크립트로 직접 호출했을 때는 데이터베이스와 테이블을 만들고 싶어요.
팁
**스크립트(script)**와 **프로그램(program)**이라는 단어를 서로 바꿔 써도 된다고 생각하면 돼요.
스크립트라는 단어는 코드가 독립적으로 쉽게 실행될 수 있다는 뜻을 자주 내포해요. 어떤 경우에는 비교적 간단한 프로그램을 가리키기도 하고요.
그러려면 특별한 변수 __name__을 if 블록 안에서 쓰면 돼요:
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)
if __name__ == "__main__":
create_db_and_tables()
__name__ == "__main__"에 대해서
__name__ == "__main__"의 주 목적은, 파일이 아래처럼 호출될 때 실행되는 코드를 가지는 거예요:
$ uv run python app.py
// 여기서 뭔가가 일어나요 ✨
...하지만 다른 파일이 아래처럼 임포트할 때는 실행되지 않아요:
from app import Hero
팁
if __name__ == "__main__":을 쓰는 그if블록을 가끔 "메인 블록(main block)"이라고 부르기도 해요.공식 이름은 (파이썬 문서에 따르면) "최상위 스크립트 환경(Top-level script environment)"이에요.
좀 더 자세히
파일 이름이 myapp.py라고 가정해 볼게요.
이렇게 실행하면:
$ uv run python myapp.py
// 이렇게 하면 create_db_and_tables() 가 호출돼요
...파이썬이 자동으로 만든 파일 안의 내부 변수 __name__의 값이 문자열 "__main__"이 돼요.
그래서 아래의 함수가:
if __name__ == "__main__":
create_db_and_tables()
...실행되는 거예요.
하지만 그 모듈(파일)을 임포트하면 이런 일이 일어나지 않아요.
그래서 importer.py라는 다른 파일이 있다고 할 때:
from myapp import Hero
# 코드가 좀 더 이어져요
...그 경우 myapp.py 안의 자동 변수 __name__의 값은 "__main__"이 아니에요.
그래서 아래 줄은:
if __name__ == "__main__":
create_db_and_tables()
...실행되지 않아요.
참고
자세한 내용은 공식 파이썬 문서를 확인해 보세요.
마지막 점검
이런 변경을 한 뒤에도 다시 실행하면, 예전과 똑같은 출력을 만들어 낼 거예요.
하지만 이제는 다른 파일에서 이 모듈에서 뭔가를 임포트할 수 있게 됐어요.
이제 코드를 마지막으로 한 번 볼게요:
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)!
-
None일 수 있는 필드를 선언하려고typing에서Optional을 임포트해요. -
sqlmodel에서 필요한 것들,Field,SQLModel,create_engine을 임포트해요. -
데이터베이스의
hero테이블을 나타내는Hero모델 클래스를 만들어요.그리고
table=True로 이 클래스를 테이블 모델로 표시해요. -
id필드를 만들어요:데이터베이스가 값을 할당하기 전까지는
None일 수 있으니Optional(Python 3.10+에서는int | None)로 표기해요.기본 키이므로
Field()에primary_key=True인자를 써요. -
name필드를 만들어요.필수 값이라 기본값이 없고,
Optional도 아니에요. -
secret_name필드를 만들어요.이것도 필수 값이에요.
-
age필드를 만들어요.필수 값이 아니고, 기본값은
None이에요.데이터베이스에서는 기본값이
None의 SQL 버전인NULL이 돼요.이 필드는
None(그리고 데이터베이스에서는NULL)이 될 수 있으니Optional(Python 3.10+에서는int | None)로 표기해요. -
데이터베이스 파일의 이름을 써요.
-
데이터베이스 파일 이름으로 데이터베이스 URL을 만들어요.
-
URL로 엔진을 만들어요.
이 시점에는 아직 데이터베이스가 생기지 않아요. 파일이나 테이블이 만들어지지 않고, (URL을 바탕으로) 이 특정 데이터베이스와의 연결을 처리하고 SQLite를 특별히 지원하는 엔진 객체만 생겨요.
-
부수 효과를 만드는 코드를 함수에 넣어요.
이 경우 데이터베이스 파일과 테이블을 만드는 한 줄이에요.
-
SQLModel.metadata에 자동으로 등록된 모든 테이블을 만들어요. -
메인 블록, 즉 "최상위 스크립트 환경(Top-level script environment)"을 추가해요.
그리고 이 파일이 아래처럼 파이썬으로 직접 호출될 때 실행할 로직을 넣어요:
$ python app.py // 모든 걸 실행하고 출력을 보여줘요...하지만 이 모듈에서 뭔가를 임포트할 때는 실행되지 않아요. 예를 들면:
from app import Hero -
이 메인 블록에서 데이터베이스 파일과 테이블을 만드는 함수를 호출해요.
이렇게 하면 아래처럼 호출했을 때:
$ python app.py // 일을 해요 ✨...데이터베이스 파일과 테이블이 만들어져요.
팁
코드 안의 번호 풍선을 클릭하면 각 줄이 하는 일을 다시 볼 수 있어요. 👆
정리(Recap)
이번 장에서는 SQLModel을 써서 데이터베이스의 테이블이 어떻게 생겨야 하는지 정의하는 법을 배웠고, SQLModel로 데이터베이스와 테이블을 직접 만들어 봤어요.
또한 나중에 재사용·공유·테스트하기 쉽도록 코드를 리팩터링했어요.
다음 장들에서는 SQLModel이 어떻게 코드에서 SQL 데이터베이스와 상호작용하는 데 도움을 주는지 보게 될 거예요. 🤓