REAL Python – FastAPI] – “풀스택 블로그 개발하기 – DB&ORM 알아보기”

REAL Python – FastAPI] – “풀스택 블로그 개발하기 – DB&ORM 알아보기”

12월 8, 2023

데이터베이스 없는 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 만 처리할 수 있죠. 데이터베이스 APIPEP 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)

위의 코드, 각각의 필드를 보기만 하더라도 어떤 것을 의미하는 지 알 것만 같지 않나요? 맞습니다. 위의 코드는 관계형 데이터베이스에서 하나의 테이블을 나타냅니다. 아래와 같은 테이블을 파이썬의 클래스로 나타낸 것입니다.

idtitlecontent
1첫 번째 블로그 게시물입니다.막상, 쓸 말이 별로 없네요.

이처럼, 복잡한 데이터베이스 관련 작업들을 ORMSQL 대신 파이썬 코드로 다룰 수 있게끔 해 주는 역할을 합니다. 여러 데이터베이스 접속을 SQLAlchemy 가 추상화해주기 때문에, 우리가 신경써야 할 것은 타겟 데이터베이스가 MySQL인지 PostgreSQL 인지가 아니라 파이썬 모델 그 자체죠. 테이블을 객체처럼 다루게 해 주는 것이 ORM 의 강력함입니다.

아직 ORM 을 통해서 많은 작업을 해 보지는 않았지만, 위의 클래스가 테이블과 매핑되는 것이라는 것을 이해한다면 앞으로의 개발이 점점 즐거워질 것이라 믿습니다.

다음 시간에는, 프로젝트를 위한 구조 잡기를 해 보겠습니다.

Leave A Comment

Avada Programmer

Hello! We are a group of skilled developers and programmers.

Hello! We are a group of skilled developers and programmers.

We have experience in working with different platforms, systems, and devices to create products that are compatible and accessible.