CRUD·삽입 (Insert/CRUD)
CRUD·삽입 (Insert/CRUD)
데이터베이스와 테이블이 준비됐으니, 이제 데이터를 넣어볼 차례예요.
아래 표가 우리가 만든 테이블의 모습이고, 여기에 추가할 데이터예요:
| id | name | secret_name | age |
|---|---|---|---|
| 1 | Deadpond | Dive Wilson | null |
| 2 | Spider-Boy | Pedro Parqueador | null |
| 3 | Rusty-Man | Tommy Sharp | 48 |
테이블과 데이터베이스 만들기
지난 장에서 멈췄던 부분부터 이어서 진행할게요.
데이터베이스와 테이블을 만들기 위해 작성했던 코드인데, 여기서 새로운 건 없어요:
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)
# More code here later 👈
if __name__ == "__main__":
create_db_and_tables()
-
typing에서Optional을 가져와서None이 될 수 있는 필드를 선언해요. -
sqlmodel에서 필요한 것들인Field,SQLModel,create_engine을 가져와요. -
데이터베이스의
hero테이블을 나타내는Hero모델 클래스를 만들어요.그리고
table=True로 이 클래스를 테이블 모델로 표시해요. -
id필드를 만들어요.데이터베이스가 값을 부여하기 전까지는
None일 수 있어서Optional로 표시해요 (Python 3.10+에서는int | None). 기본 키(primary key) 이니Field()를 쓰면서primary_key=True인자를 넘겨요. -
name필드를 만들어요.필수 필드라서 기본값이 없고,
Optional도 아니에요. -
secret_name필드를 만들어요.이것도 필수예요.
-
age필드를 만들어요.필수는 아니고 기본값이
None이에요. 데이터베이스에서 기본값은None에 해당하는 SQL 값인NULL이 돼요. 이 필드가None(데이터베이스에선NULL)이 될 수 있으니Optional로 표시해요 (Python 3.10+에서는int | None). -
데이터베이스 파일 이름을 적어요.
-
파일 이름으로 데이터베이스 URL을 만들어요.
-
URL로 엔진(engine)을 만들어요.
이 시점에는 아직 데이터베이스가 만들어지지 않아요. 파일도 테이블도 아직 없고, 이 특정 데이터베이스와 연결을 담당할 엔진 객체만 생기는 거예요. URL에 기반해 SQLite를 지원하는 엔진이에요.
-
부수 효과(side effect)를 만들어내는 코드는 함수 안에 둬요.
여기서는 데이터베이스 파일과 테이블을 만드는 한 줄뿐이에요.
-
SQLModel.metadata에 자동으로 등록된 모든 테이블을 만들어요. -
main블록, 즉 "최상위 스크립트 환경"을 추가해요.그래서 이 모듈을 Python으로 직접 호출할 때 실행할 로직을 넣어요. 예를 들면:
$ python app.py
# Execute all the stuff and show the output
...하지만 다른 곳에서 `from app import Hero`처럼 이 모듈에서 무언가를 import할 때는 실행되지 않아요.
-
이
main블록에서 데이터베이스 파일과 테이블을 만드는 함수를 호출해요.이렇게 하면 아래처럼 호출했을 때:
$ python app.py
# Doing stuff ✨
...데이터베이스 파일과 테이블이 만들어져요.
이제 데이터베이스와 테이블을 만들 수 있으니, 이 지점에서부터 같은 파일에 데이터를 만드는 코드를 더 추가할게요.
SQL로 데이터 만들기
Python 코드로 넘어가기 전에, SQL로는 데이터를 어떻게 만드는지 먼저 볼게요.
Deadpond 레코드(행, row)를 데이터베이스에 넣는다고 해볼게요.
이렇게 SQL 코드로 할 수 있어요:
INSERT INTO "hero" ("name", "secret_name")
VALUES ("Deadpond", "Dive Wilson");
이 코드의 의미를 풀어보면 대략 이래요:
안녕 SQL 데이터베이스 👋,
"hero"테이블에INSERT(레코드/행 만들기)를 해줘.이 특정 컬럼들에 값을 넣은 행 하나를 만들어줘:
"name""secret_name"그리고 이 컬럼들에 넣을 값은:
"Deadpond""Dive Wilson"
DB Explorer for SQLite에서 시도해보기
그 SQL 문장을 DB Explorer for SQLite에서 직접 시도해볼 수 있어요.
Open Database를 클릭하고 아까 만든 database.db 파일을 선택해서, 우리가 만든 그 데이터베이스를 열어야 해요.
팁
hero테이블이 있는database.db파일이 없다면, 위쪽의 Python 프로그램을 실행해서 다시 만들면 돼요. 👆
그리고 Execute SQL 탭으로 가서 위의 SQL을 복사해요.
이렇게 보일 거예요:

Execute all ▶ 버튼을 클릭해요.
그러면 Browse Data 탭으로 가서, 방금 만든 레코드/행을 확인할 수 있어요:

데이터베이스 안의 데이터와 코드 안의 데이터
프로그래밍 언어로 데이터베이스(SQL이든 다른 종류든)를 다룰 때, 메모리 안의 데이터는 항상 양쪽에 존재해요 — 코드에서 만드는 객체와 변수에 있는 데이터, 그리고 데이터베이스 안에 있는 데이터.
우리는 끊임없이 데이터베이스에서 데이터의 일부를 가져와서(getting) 메모리의 변수에 담아요.
마찬가지로 코드에서 데이터를 담은 변수와 객체를 만들고, 그걸 나중에 데이터베이스에 저장하고 싶을 때 보내요(send).
어떤 경우에는 메모리에 데이터를 만들고, 데이터베이스에 저장하기 전에 그걸 바꾸고 업데이트할 수도 있어요.
심지어 코드의 어떤 로직에 따라 그 데이터를 더 이상 데이터베이스에 저장하지 않기로 하고 그냥 없애버릴 수도 있어요. 🔥 그 경우엔 데이터를 메모리에서만 다뤘지, 데이터베이스로 왔다 갔다 보내지 않은 거예요.
SQLModel은 (실제로는 SQLAlchemy를 통해) 이런 상호작용을 가능한 한 단순하고 직관적이며, "프로그래밍에 가깝게" 만들어주기 위해 할 수 있는 모든 걸 해요. ✨
하지만 어떤 순간의 데이터가 둘 중 어디(메모리인지 데이터베이스인지)에 있을 수 있다는 그 구분은 항상 존재해요. 그리고 이걸 염두에 두는 게 중요해요. 🤓
Python과 SQLModel로 데이터 만들기
이제 그 같은 행을 Python으로 만들어볼게요.
먼저 database.db 파일을 지워서 깨끗한 상태에서 시작해요.
메모리 안의 데이터로 실행되는 Python 코드와, 독립적인 시스템(외부 SQLite 파일이든, 외부 데이터베이스 서버든)인 데이터베이스 때문에, 두 단계를 거쳐야 해요:
- 데이터를 Python 안(변수)에서 메모리에 만든다
- 그 데이터를 데이터베이스에 저장/전송한다
모델 인스턴스 만들기
첫 단계, 메모리에 데이터를 만드는 것부터 시작할게요.
우리는 이미 데이터베이스의 hero 테이블을 나타내는 Hero 클래스를 만들었어요.
각 인스턴스는 데이터베이스의 한 행에 해당하는 데이터를 나타내요.
그래서 첫 단계는 단순히 Hero의 인스턴스를 만드는 거예요.
영웅 3명을 위해 3개를 바로 만들게요:
# Code above omitted 👆
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
# Code below omitted 👇
👀 전체 파일 미리보기
from sqlmodel import Field, Session, 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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def main():
create_db_and_tables()
create_heroes()
if __name__ == "__main__":
main()
팁
이 파일에서 위에 생략된 코드는 이 장 맨 위에서 봤던 것과 같은 코드예요.
Hero모델을 만들 때 썼던 바로 그 코드예요.이걸
create_heroes()함수 안에 넣어서, 나중에 완성되면 호출하려는 거예요.코드를 대화형으로 실행해 보는 중이라면, 그냥 직접 적어도 돼요.
Session 만들기
지금까지는 데이터베이스와 상호작용할 때 엔진(engine) 만 사용했어요.
엔진은 모든 코드가 공유하는 단일 객체로, 데이터베이스와 통신하고 (PostgreSQL이나 MySQL 같은 서버 데이터베이스를 쓸 때) 연결을 관리하는 등의 일을 담당해요.
하지만 SQLModel로 작업할 때는 대부분 그 위에 얹혀 있는 또 다른 도구인 Session을 사용하게 돼요.
앱 전체에 하나뿐인 엔진과는 달리, 우리는 함께 묶여야 하는 각 작업 그룹마다 새로운 세션(session) 을 만들어요.
실제로 세션은 엔진이 필요하고 그것을 사용해요.
예를 들어 웹 애플리케이션이라면 보통 요청(request)마다 세션 하나를 두게 돼요.
앱의 모든 코드, 모든 곳에서 같은 엔진을 재사용해요 (모든 요청이 공유). 하지만 각 요청마다 새 세션을 만들어 쓰고, 요청이 끝나면 세션을 닫아요.
첫 단계는 Session 클래스를 import하는 거예요:
from sqlmodel import Field, Session, SQLModel, create_engine
# Code below omitted 👇
👀 전체 파일 미리보기
from sqlmodel import Field, Session, 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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
session = Session(engine)
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
session.close()
def main():
create_db_and_tables()
create_heroes()
if __name__ == "__main__":
main()
그 다음 새 세션을 만들 수 있어요:
# Code above omitted 👆
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
session = Session(engine)
# Code below omitted 👇
새 Session은 engine을 매개변수로 받아요. 그리고 내부적으로 그 엔진을 사용해요.
팁
나중에
with블록으로 세션을 만드는 더 나은 방법을 볼 거예요.
세션에 모델 인스턴스 추가하기
이제 영웅 모델 인스턴스(메모리 안의 객체) 몇 개와 세션이 있으니, 다음 단계는 그걸 세션에 추가하는 거예요:
# Code above omitted 👆
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
session = Session(engine)
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
# Code below omitted 👇
이 시점까지 영웅들은 아직 데이터베이스에 저장되지 않았어요.
그리고 이것이 바로 엔진과 별개인 세션이 의미 있는 경우 중 하나예요.
세션은 나중에 데이터베이스에 저장해야 할 모든 객체를 메모리에 담고 있어요.
준비가 되면 그 변경사항을 커밋하고, 그러면 세션이 내부의 엔진을 써서 적절한 SQL을 데이터베이스로 보내 모든 데이터를 저장해요. 그렇게 모든 행이 한 번에 만들어져요.
이렇게 하면 데이터베이스와의 상호작용이 더 효율적이 돼요 (거기에 추가 이점도 있고요).
기술 세부사항
세션은 새 트랜잭션을 만들고, 모든 SQL 코드를 그 트랜잭션 안에서 실행해요.
이렇게 하면 데이터가 단일 배치로 저장되고, 전부 성공하거나 전부 실패하게 돼서 데이터베이스가 손상된 상태로 남지 않아요.
세션 변경사항 커밋하기
이제 영웅들이 세션에 있고, 그걸 모두 데이터베이스에 저장할 준비가 됐으니 변경사항을 커밋할 수 있어요:
# Code above omitted 👆
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
session = Session(engine)
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
# Code below omitted 👇
이 한 줄이 실행되면, 세션이 엔진을 써서 해당 SQL을 보내 모든 데이터를 데이터베이스에 저장해요.
스크립트로 영웅 만들기
영웅을 만드는 함수가 준비됐어요.
이제 이 프로그램을 Python으로 직접 실행할 때 그 함수를 호출하도록 하면 돼요.
이미 이런 main 블록이 있었죠:
if __name__ == "__main__":
create_db_and_tables()
거기에 새 함수를 추가할 수도 있어요. 이렇게요:
if __name__ == "__main__":
create_db_and_tables()
create_heroes()
하지만 조금 더 정리하기 위해, 독립 스크립트로 호출됐을 때 실행돼야 할 모든 코드를 담을 새 함수 main()을 만들고, 거기에 이전 함수 create_db_and_tables()를 넣고, 새 함수 create_heroes()를 추가할게요:
# Code above omitted 👆
def main():
create_db_and_tables()
create_heroes()
# Code below omitted 👇
그리고 main 블록에서 그 단일 함수 main()을 호출하면 돼요:
# Code above omitted 👆
def main():
create_db_and_tables()
create_heroes()
if __name__ == "__main__":
main()
스크립트로 호출됐을 때 일어나야 하는 모든 걸 단일 함수에 모아두면, 나중에 코드를 쉽게 더 추가할 수 있어요.
또한 필요하다면 다른 코드에서도 같은 main() 함수를 import해서 쓸 수 있어요.
스크립트 실행하기
이제 우리 프로그램을 콘솔에서 스크립트로 실행할 수 있어요.
엔진을 echo=True로 만들었기 때문에, 실행 중인 모든 SQL 코드가 출력돼요:
$ uv run python app.py
# Some boilerplate, checking that the hero table already exists
INFO Engine BEGIN (implicit)
INFO Engine PRAGMA main.table_info("hero")
INFO Engine [raw sql] ()
INFO Engine COMMIT
# BEGIN a transaction automatically ✨
INFO Engine BEGIN (implicit)
# Our INSERT statement, it uses VALUES (?, ?, ?) as parameters
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
# ...and these are the parameter values 🚀
INFO Engine [generated in 0.00013s] ('Deadpond', 'Dive Wilson', None)
# Again, for Spider-Boy
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
INFO Engine [cached since 0.000755s ago] ('Spider-Boy', 'Pedro Parqueador', None)
# And now for Rusty-Man
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
INFO Engine [cached since 0.001014s ago] ('Rusty-Man', 'Tommy Sharp', 48)
# All good? Yes, commit this transaction! 🎉
INFO Engine COMMIT
Git을 써본 적이 있다면, 이건 아주 비슷하게 동작해요.
session.add()로 새 객체(모델 인스턴스)를 세션에 추가해요 (git add와 비슷해요).
그러면 저장할 준비가 됐지만 아직 저장되진 않은 데이터 그룹이 생겨요.
수정을 더 하거나, 객체를 더 추가하는 등의 일을 할 수 있어요.
그리고 준비가 되면 모든 변경사항을 한 단계로 커밋해요 (git commit과 비슷해요).
세션 닫기
세션은 엔진의 커넥션 같은 일부 리소스를 보유해요.
그래서 세션 사용이 끝나면 닫아서 그 리소스를 놓아주고 정리를 끝내야 해요:
# Code above omitted 👆
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
session = Session(engine)
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
session.close()
# Code below omitted 👇
그런데 세션을 닫는 걸 잊으면 어떻게 될까요?
아니면 코드에 예외가 생겨서 session.close()에 도달하지 못하면요?
그걸 위해 세션을 만들고 닫는 더 나은 방법이 있는데, 바로 with 블록을 쓰는 거예요. 👇
with 블록 안의 세션
Session이 어떻게 동작하고, 직접 만들고 닫는 법을 아는 건 좋아요. 예를 들어 대화형 세션(가령 Jupyter)에서 코드를 탐색하고 싶다면 유용할 수 있어요.
하지만 세션을 다루는 더 나은 방법이 있는데, with 블록을 쓰는 거예요:
# Code above omitted 👆
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
# Code below omitted 👇
이건 세션을 직접 만들고 직접 닫는 것과 같아요. 여기서는 with 블록을 써서, with 블록이 시작될 때 세션이 자동으로 만들어져 session 변수에 할당되고, with 블록이 끝나면 자동으로 닫혀요.
게다가 코드에 예외가 생겨도 잘 동작해요. 😎
전체 코드 검토하기
이제 이 파일 전체를 마지막으로 살펴볼게요. 🔍
Hero 모델 클래스, 엔진, 데이터베이스와 테이블을 만드는 첫 부분은 이미 다 알고 있죠.
새로운 코드에 집중해볼게요:
from sqlmodel import Field, Session, 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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def main():
create_db_and_tables()
create_heroes()
if __name__ == "__main__":
main()
-
이 로직을 한데 모으기 위해 함수
create_heroes()를 사용해요. -
Hero모델의 객체/인스턴스를 각각 만듦니다.각각은 한 행에 해당하는 데이터를 나타내요.
-
with블록으로engine을 사용해Session을 만들어요.새 세션은 변수
session에 할당돼요. 그리고with블록이 끝나면 자동으로 닫혀요. -
각 객체/인스턴스를 세션에 추가해요.
이 객체들은 각각 데이터베이스의 한 행을 나타내요. 모두 세션 안에서 저장되기를 기다리고 있어요.
-
변경사항을 데이터베이스에 커밋해요.
이게 실제로 데이터를 데이터베이스로 보내요. 자동으로 트랜잭션을 시작하고 모든 데이터를 단일 배치로 저장해요.
-
이 시점에,
with블록이 끝난 후 세션은 자동으로 닫혀요. -
콘솔에서 스크립트로 호출됐을 때 실행돼야 할 모든 코드를 담은
main()함수가 있어요.이렇게 하면 나중에 이 함수에 코드를 더 추가할 수 있어요. 그리고 이 함수
main()을 아래의main블록에 넣어요. 단일 함수이기 때문에 다른 Python 파일에서 import해서 직접 호출할 수도 있어요. -
이
main()함수 안에서 데이터베이스와 테이블도 만들고 있어요.이전 버전에서는 이 함수가
main블록에서 직접 호출됐어요. 이제는main()함수 안에서 호출돼요. -
그리고 이제 이
main()함수에서 영웅들도 만들고 있어요. -
여전히
main블록이 있어서, 프로그램이 커맨드라인에서 스크립트로 실행될 때 코드를 실행해요. 예를 들면:
$ python app.py
# Do whatever is in the main block 🚀
-
이제 콘솔에서 프로그램을 실행할 때 실행돼야 할 모든 코드를 담은
main()함수가 단 하나 있어요.그래서
main블록에 필요한 건 이게 전부예요.main()함수만 호출하면 돼요.
팁
각 번호 버블을 클릭해서 각 줄이 무슨 일을 하는지 검토해보세요. 👆
이제 이걸 app.py 파일에 넣고 Python으로 실행할 수 있어요. 그러면 위에 나온 것과 같은 출력을 볼 수 있어요.
그 후 DB Browser for SQLite로 데이터베이스를 열면, Browse Data 탭에서 방금 만든 데이터를 볼 수 있어요:

다음 단계
이제 데이터베이스에 행을 추가하는 방법을 알게 됐어요. 🎉
이제 id 필드가 데이터베이스에서는 기본 키라서 NULL이 될 수 없지만, Python 코드에서는 실제로 None이 될 수 있는 이유를 더 잘 이해할 좋은 시점이에요.
그 이야기는 다음 장에서 할게요. 🚀