FastAPI를 이용한 웹 서비스 구현 연습_2

Frye 'de Bacon·2023년 10월 25일

본 시리즈는 '박응용' 님의 '점프 투 FastAPI'를 바탕으로 학습 및 실습한 내용을 정리한 것입니다.


구현 및 파인튜닝한 모델을 사용한 웹 서비스 구현을 위해 FastAPI의 학습 필요성을 느껴 학습 과정을 정리합니다. 내용의 정확성이나 이론적인 부분은 당연히 원본 페이지를 참조하시는 게 좋고, 본 시리즈에서는 구현 도중 발생하는 문제 등을 해결하는 과정을 함께 기록하여 '처음부터 끝까지 따라 할 수 있는' 시리즈를 만드는 것을 목표로 합니다(물론 제1목표는 학습 내용 기록입니다).


1. FastAPI의 구조 설계

이번 프로젝트는 질문과 답변을 작성하는 게시판을 구현하는 것을 목표로 한다. 본격적인 구현에 앞서 미리 프로젝트의 전체 구조를 설계해 두는 것이 좋다. 대략적인 구조는 다음과 같다.

├── main.py
├── database.py
├── models.py
├── domain
│   ├── answer
│   ├── question
│   └── user
└── frontend

각 구조는 다음과 같은 역할을 한다.

  1. main.py : 프로젝트의 핵심 객체로서 프로젝트의 전체적인 환경 설정
  2. database.py : 데이터베이스를 사용하기 위한 변수 및 함수 등을 정의하고, 접속할 데이터베이스의 주소와 사용자, 비밀번호 등을 관리
  3. models.py : 본 프로젝트는 ORM을 지원하는 파이썬 데이터베이스 도구인 SQLAlchemy를 사용하는데, SQLAlchemy는 모델 기반으로 데이터베이스를 처리한다. 따라서 모델 클래스를 정의할 models.py 파일이 필요하다(자세한 내용은 추후 설명한다).
  4. domain 디렉토리
    API를 구성하는 '질문', '답변', '사용자'라는 3개의 도메인을 두어 그와 관련된 파일을 작성한다. 각 도메인은 API를 생성하기 위해 3개의 파일이 필요하다.
    4-1. 라우터 파일 : URL과 API의 전체적인 동작 관리
    4-2. 데이터베이스 처리 파일 : 데이터의 생성(Create), 조회(Read), 수정(Update), 삭제(Delete)를 처리(CRUD)
    4-3. 입출력 관리 파일 : 입력 데이터 및 출력 데이터의 스펙을 정의하고 검증
    따라서 각 도메인별 3개, 총 9개의 파일이 생성된다.
  5. Frontend 디렉토리
    Svelte 프레임워크를 설치한 디렉토리로, 프론트엔드의 루트 디렉토리

2. 모델로 데이터베이스 관리하기

질문 답변 게시판의 구현을 위해서는 데이터를 저장 및 조회하는 기능을 구현해야 하며, 따라서 데이터베이스를 사용해야 한다. 일반적으로는 SQL을 이용해 데이터베이스를 다루지만, ORM(Object Relational Mapping)을 이용하면 파이썬 문법만으로도 데이터베이스를 다룰 수 있다. 따라서 파이썬 ORM 중 가장 많이 사용되는 SQLAlchemy를 이용해 데이터베이스를 다루도록 한다(내 경우 간단한 SQL은 구현 가능하지만 일단 튜토리얼대로 구현해보도록 한다).

SQLAlchemy 설치

다음 명령어를 통해 SQLAlchemy 라이브러리를 설치한다.

(practice_1) C:\workspace\fastapi_practice\practice_1> pip install sqlalchemy

설정 파일 추가

FastAPI에 ORM을 적용하기 위해 데이터베이스 설정을 해야 한다. 메인 디렉토리에 database.py 파일을 생성한 후 다음 코드를 입력한다.

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./practice_1.db"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

각 코드의 역할은 다음과 같다.

  1. SQLALCHEMY_DATABASE_URL : 데이터베이스 접속 주소로, 'sqlite:///./practice_1.db'는 sqlite3 데이터베이스 파일을 프로젝트 루트 디렉토리에 저장한다는 의미이다.
  2. SessionLocal : 데이터베이스에 접속하기 위해 필요한 클래스로, create_engine, sessionmaker 등은 SQLAlchemy 데이터베이스를 사용하기 위해 따라야 하는 규칙이다. 이때 autocommit을 False로 설정하는 것은 commit 없이 데이터베이스에 변경사항이 저장되는 것을 방지하기 위한 것인데, 이를 만약 True로 설정할 경우 변경 사항이 즉시 데이터베이스에 적용되며 rollback이 불가능하다.
  3. create_engine : 커넥션 풀을 생성한다. 커넥션 풀은 데이터베이스에 접속하는 객체를 일정 개수만큼 만들고 돌려 가며 사용하는 것을 말한다.
  4. declarative_base : 데이터베이스 모델을 구성할 때 사용되는 클래스이다.

3. 모델 만들기

모델 속성의 구상

질문과 답변 모델에 필요한 속성(attribute)은 각각 다음과 같을 것이다.
[질문 모델 속성]

속성명설명
id질문 데이터의 고유 번호(Primary key)
subject질문의 제목
content질문의 내용
create_date질문 작성 일시

[답변 모델 속성]

속성명설명
id답변 데이터의 고유 번호(Primary key)
question_id질문 데이터의 고유 번호(Foreign key)
content답변의 내용
create_date답변 작성 일시

질문 모델 생성

상기에서 구상한 속성을 바탕으로 모델을 정의해 보자. 모델 속성을 정의하기 위한 models.py 파일을 생성하고 질문 모델인 Question 클래스를 선언한다.

from sqlalchemy import Column, Integer, String, Text, DateTime
from database import Base

class Question(Base):
    __tablename__ = "Question"

    id = Column(Integer, primary_key=True)
    subject = Column(String, nullable=False)
    content = Column(Text, nullable=False)
    create_date = Column(DateTime, nullable=False)

모델 클래스는 database.py에서 정의한 Base 클래스를 상속하여 만들어야 한다. __tablename__은 모델에 의해 관리되는 테이블의 이름이며, 각 속성은 Column으로 생성한다. 이때 Column의 첫 번째 인자는 해당 속성의 데이터 타입을 의미하며, 추가로 입력되는 속성은 다음과 같다.

  1. primary_key : 해당 속성을 '기본 키'로 만드는 인자로, 이를 True로 설정하면 해당 속성은 데이터베이스에서 중복되는 값을 가질 수 없게 된다. 즉, 해당 인스턴스를 구분할 수 있게 만드는 일종의 index 역할을 하게 된다.
  2. nullable : 해당 속성이 null값이 될 수 있는지 여부를 설정한다. nullable을 False로 설정할 경우 해당 속성에는 반드시 데이터가 입력되어야 한다. 기본값은 True이므로 주의할 것.

답변 모델 생성

마찬가지의 방법으로 Answer 클래스도 생성한다.

from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.orm import relationship

from database import Base

class Question(Base):
    __tablename__ = "Question"

    id = Column(Integer, primary_key=True)
    subject = Column(String, nullable=False)
    content = Column(Text, nullable=False)
    create_date = Column(DateTime, nullable=False)

class Answer(Base):
    __tablename__ = "Answer"

    id = Column(Integer, primary_key=True)
    content = Column(String, nullable=False)
    create_date = Column(DateTime, nullable=False)
    question_id = Column(Integer, ForeignKey("question.id"))
    question = relationship("Question", backref="answers")

다른 속성은 앞서 질문 모델과 같으므로 넘어가고, 여기서 중요한 것은 question_id와 question 속성이다.

  1. question_id : 답변과 질문을 연결하기 위해 추가한 속성으로, 이 답변이 어떤 질문에 대한 답변인지 알아야 하므로 질문 id의 속성이 필요하다. 이처럼 모델을 서로 연결할 때는 ForeignKey를 사용한다. ForeignKey의 첫 번째 인자인 "Question.id"는 Question 테이블의 id 컬럼을 의미한다(바로 아래에 정의되는 question 객체가 아니다). 즉 Answer 모델의 question_id 속성은 Question 테이블의 id 컬럼과 연결된다는 의미이다.
  2. question : 답변에서 질문 모델을 참조하기 위해 추가된 속성이다. relationship을 이용해 question 속성을 생성하면, 답변 객체에서 연결된 질문의 제목을 Answer.question.subject와 같이 참조할 수 있다. 이때 첫 번째 인자는 참조할 모델의 이름이고, 두 번째 인자인 backref는 역참조 설정이다. 이를 이용하여 question.answers와 같은 코드로 해당 질문에 달린 답변들을 참조할 수 있다.

4. 모델을 이용해 테이블 자동으로 생성하기

모델을 구상하고 생성했으니, 이를 이용해 데이터베이스 테이블을 생성해 보자. 이를 위해 SQLAlchemy의 alembic을 이용한다. alembic은 SQLAlchemy로 작성한 모델을 기반으로 데이터베이스를 관리할 수 있도록 도와주는 도구이다.

alembic 설치 및 초기화

# alembic 설치
(practice_1) C:\workspace\fastapi_practice\practice_1> pip install alembic
# 초기화
(practice_1) C:\workspace\fastapi_practice\practice_1> alembic init migrations

설치 및 초기화를 완료하면 practice_1 디렉토리 아래에 migrations라는 디렉토리와 alembic.ini 파일이 생성된다. migrations 디렉토리는 alembic 도구 사용 시 생성되는 리비전 파일들을 저장하는 디렉토리이고, alembic.ini 파일은 alembic의 환경 설정 파일이다.
리비전 파일이란 alembic을 이용해 테이블을 생성 혹은 변경할 때마다 생성되는 작업 파일을 말한다.
이제 alembic.ini 파일을 열어 다음과 같이 수정한다.

...
sqlalchemy.url = sqlite:///./myapi.db
...

migrations 폴더의 env.py 파일도 다음과 같이 수정한다.

...
import models
...
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = models.Base.metadata
...

리비전 파일 생성

터미널에서 다음 명령을 수행해 리비전 파일을 생성한다.

(practice_1) C:\workspace\fastapi_practice\practice_1> alembic revision --autogenerate

그러면 다음과 같은 메시지와 함께 migrations\versions 디렉토리에 리비전 파일이 생성된 것을 확인할 수 있다.

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'Question'
INFO  [alembic.autogenerate.compare] Detected added table 'Answer'
Generating C:\workspace\fastapi_practice\practice_1\migrations\versions\a2bf9f6ae936_.py ...  done

이렇게 생성된 리비전 파일을 실행한다.

(practice_1) C:\workspace\fastapi_practice\practice_1> alembic upgrade head

그러면 다음과 같은 메시지와 함께 practice_1 디렉토리에 practice_1.db라는 파일이 생성된 것을 확인할 수 있다. 바로 이 파일이 SQLite의 데이터베이스 파일이다.

INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> a2bf9f6ae936, empty message

생성된 테이블 확인

생성한 데이터베이스에 정말로 Question과 Asnwer 테이블이 생성되었는지 확인해보자 DB Brower for SQLite를 설치해 확인해보면 다음과 같이 Answer와 Question 테이블이 생성된 것을 확인할 수 있다.

profile
AI, NLP, Data analysis로 나아가고자 하는 개발자 지망생

0개의 댓글