REAL Python – FastAPI] – “풀스택 블로그 개발하기 – DB&ORM 알아보기”
REAL Python – FastAPI] – “풀스택 블로그 개발하기 – DB&ORM 알아보기”
데이터베이스 없는 API
이전 시간, 우리는 간단한 JSON 포맷의 응답을 하는 웹 애플리케이션 서버를 구축했습니다. 블로그와는 거리가 꽤 먼 API였지만, 그 시작을 잘 끝마쳤죠. 이번 시간에는 블로그를 위한 간단한 API 를 개발하겠습니다.
잠시 눈을 감고 머릿속으로 완성된 블로그의 모습을 생각해 봅시다.
개발자스러운 느낌이 나는 멋들어진 헤더. 그 아래에는 내가 작성한 여러 개의 글 제목들이 보입니다. 독자는 커피 한 잔을 마시며 그 글을 누릅니다. 누가 썼는지는 몰라도 흥미로운 주제, 땔감스럽지 않은 내용, 읽기 쉬운 글들이 펼쳐집니다. 독자는 커피를 다시 한번 후릅거리고 안경을 고쳐 쓰며 블로그를 즐겨찾기에 추가합니다.
여기서 중요한 것은 우리가 머릿속으로 블로그 게시물 하나에는 글 제목과 글 내용이 있다 라는 것을 떠올렸다는 것입니다. 파이썬으로 이런 게시물들을 저장하려면 어떻게 하면 될까요? dict
라는 자료구조를 사용할 수 있습니다.
post = {
"title": "Blog Post 1",
"content": "This is the content of the first blog post",
}
.. 게시물은 여러 개가 될 수 있으므로, 우리는 위의 게시물 하나를 리스트로 관리할 수 있습니다.
posts = [
{
"title": "Blog Post 1",
"content": "This is the content of the first blog post",
},
{
"title": "Blog Post 2",
"content": "This is the content of the second blog post",
},
{
"title": "Blog Post 3",
"content": "This is the content of the third blog post",
},
]
좋아요. 허술해 보이지만 모냥은 갖춘 블로그 게시물을 세 개 생성했습니다. 이제 FastAPI
를 사용해서 가지고 있는 게시물들을 표현해줄 수 있습니다.
from fastapi import FastAPI
app = FastAPI()
posts = [
{
"title": "Blog Post 1",
"content": "This is the content of the first blog post",
},
{
"title": "Blog Post 2",
"content": "This is the content of the second blog post",
},
{
"title": "Blog Post 3",
"content": "This is the content of the third blog post",
},
]
@app.get("/api/posts/")
async def get_posts():
return posts
@app.get("/api/posts/{post_id}")
async def get_post(post_id: int):
return posts[post_id - 1]
..그리고, 터미널에 uvicorn app:app --reload
을 입력한 후 http://127.0.0.1:8000/docs
로 이동해 보세요.
이렇게 잘 만들어졌네요.
그러면 “게시물을 작성하는 경우” 는 어떨까요? 게시물 목록은 파이썬의 리스트로 관리되고 있으니까, 리스트에 새로운 딕셔너리를 담아서 넣으면 되지 않겠어요? 아래의 코드를 앱에 추가합니다.
@app.post("/api/posts/")
async def add_post(post: dict):
posts.append(post)
return posts[-1]
그러면 POST 에 대한 문서화가 새로 추가되었을 겁니다.
try it out 버튼을 눌러 새로운 내용을 작성하고, execute 버튼을 눌러보세요.
게시물이 잘 생성되었다고 알려주네요.
게시물 목록 API 를 확인해 보면, 우리가 작성한 새로운 글이 잘 추가된 것을 확인할 수 있습니다. 이런 식으로 파이썬의 자료구조를 잘 활용해서 저자도 추가하고, 태그도 추가하고, 카테고리, 댓글도 추가하면 블로그 게시판 개발 끝 아니겠어요?
데이터베이스의 등장
맞아요. 위의 글을 읽으시며 어이없음을 느끼셨다면 정상입니다. 왜나하면 우리가 위에서 추가한 새로운 게시물은 어딘가에 영원히 저장되는 것이 아니거든요. 앱을 종료했다가 다시 켠 후, 게시물 목록을 확인한다면 여전히 3개의 게시물만 남아있을 겁니다.
그 이유는 우리가 생성했던 게시물은 단지 컴퓨터의 메모리 안에만 존재했기 때문입니다. 앱을 종료하고 재시작할 때, 메모리는 초기화되어버려 posts
안에는 코드로 작성해 뒀던 세 개의 게시물만 남아있게 되어버린다는 것이죠.
이러한 문제를 어떻게 해결할까요?
관계형 데이터베이스 는 이런 문제를 해결할 수 있는 능력이 있습니다. 데이터들을 구조화하고, 효율적인 읽기&쓰기를 수행할 수 있는 능력이 있죠. 관계형 데이터베이스의 종류에는 여러 가지가 있지만, 이번 시간에는 그 중에서도 간단한 데이터베이스인 SQLite
를 사용하겠습니다. 아래의 코드를 app = FastAPI()
등장 이전에 넣어 보세요.
######
# DB #
######
con = sqlite3.connect("blog.db")
cur = con.cursor()
cur.execute(
"""
CREATE TABLE IF NOT EXISTS posts
(
id INTEGER PRIMARY KEY,
title TEXT,
content TEXT
)
"""
)
execute
에 전달된 문자열은 관계형 데이터베이스를 다루기 위한 SQL
문법입니다. SQLite
, MySQL, MariaDB
와 같은 관계형 데이터베이스들은 python
이 아니라 SQL
문법을 사용해 다루어져야 하죠.
from fastapi import FastAPI
import sqlite3
######
# DB #
######
con = sqlite3.connect("blog.db")
cur = con.cursor()
cur.execute(
"""
CREATE TABLE IF NOT EXISTS posts
(
id INTEGER PRIMARY KEY,
title TEXT,
content TEXT
)
"""
)
#######
# API #
#######
app = FastAPI()
@app.get("/api/posts/")
async def get_posts():
cur.execute("SELECT id, title, content FROM posts")
return [
{
"id": row[0],
"title": row[1],
"content": row[2],
}
for row in cur.fetchall()
]
@app.post("/api/posts/")
async def add_post(post: dict):
cur.execute(
"INSERT INTO posts (title, content) VALUES (?, ?)",
(post["title"], post["content"]),
)
con.commit()
return {
"title": post["title"],
"content": post["content"],
}
@app.get("/api/posts/{post_id}")
async def get_post(post_id: int):
cur.execute("SELECT * FROM posts WHERE id = ?", (post_id,))
return {
"title": cur.fetchone()[1],
"content": cur.fetchone()[2],
}
위처럼, 우리는 SQL
을 이용해서 데이터베이스에 게시물 데이터를 저장하고, 게시물 데이터를 수정하거나 삭제하는 등의 작업을 수행할 수 있습니다. 실제로 게시물을 몇 개 생성하고, 삭제하는 작업을 해 보세요. 이전과는 다르게 게시물들은 삭제되지 않고 남아있을 겁니다.
하지만 여전히 개선할 점들은 있어 보입니다.
- 데이터베이스의 변경에 자유롭지 못합니다. 위의 경우에는
sqlite
만 처리할 수 있죠. 데이터베이스API
인PEP 249
가 존재하지만, 경우에 따라 데이터베이스를 바꿀 때 코드의 변경이 이루어져야 할 수 있습니다. SQL injection
공격에 취약할 수 있습니다.- 위처럼 실제 sql 문을 수행하여 데이터베이스 작업을 수행하고 – 예외가 발생한다면 그것을 처리하고 – 그렇지 않다면 그 결과를 python 객체 등으로 변환하여 응답하는 과정은 계속 반복될 겁니다.
ORM
의 등장
ORM 을 사용하는 것은 분명히 유용하지만, 그것을 사용하는 데에 단점도 분명히 존재합니다. 이 게시물이 독자들에게 “ORM 은 원시 SQL 을 몰라도 된다!” 나 “ORM 이 원시 SQL 을 사용하는 것보다 우월하다!” 와 같은 생각들을 하지 않게 만들기를 바랍니다.
위의 문제를 해결하는 데에 ORM
이라는 것이 등장합니다. 실습을 위해서, poetry add sqlalchemy
을 터미널에 입력하여 sqlalchemy
를 설치해 주세요.
그리고 아래와 같은 클래스를 하나 살펴봅시다. (실습하지 않아도 됩니다. 나중에 더 복잡한 모델을 다룰 것이니까요!)
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String)
content = Column(String)
위의 코드, 각각의 필드를 보기만 하더라도 어떤 것을 의미하는 지 알 것만 같지 않나요? 맞습니다. 위의 코드는 관계형 데이터베이스에서 하나의 테이블을 나타냅니다. 아래와 같은 테이블을 파이썬의 클래스로 나타낸 것입니다.
id | title | content |
---|---|---|
1 | 첫 번째 블로그 게시물입니다. | 막상, 쓸 말이 별로 없네요. |
이처럼, 복잡한 데이터베이스 관련 작업들을 ORM
은 SQL
대신 파이썬 코드로 다룰 수 있게끔 해 주는 역할을 합니다. 여러 데이터베이스 접속을 SQLAlchemy
가 추상화해주기 때문에, 우리가 신경써야 할 것은 타겟 데이터베이스가 MySQL인지 PostgreSQL 인지가 아니라 파이썬 모델 그 자체죠. 테이블을 객체처럼 다루게 해 주는 것이 ORM
의 강력함입니다.
아직 ORM 을 통해서 많은 작업을 해 보지는 않았지만, 위의 클래스가 테이블과 매핑되는 것이라는 것을 이해한다면 앞으로의 개발이 점점 즐거워질 것이라 믿습니다.
다음 시간에는, 프로젝트를 위한 구조 잡기를 해 보겠습니다.