Skip to content

게시판 서비스 FastAPI 벡엔드

개요

GitHub 저장소 링크

  • FastAPI 기반 비동기 백엔드 서버

기술 스택

  • 백엔드 서버 구성: FastAPI Gunicorn Uvicorn
  • 데이터베이스: MariaDB
  • 캐시 서버: Redis
  • 모니터링: Prometheus Logstash
  • 배포: Docker

서비스 설명

  • 절차적 프로그래밍을 기반으로 한 도메인 주도의 레이어드 아키텍처 구성을 통해 코드 가독성 확보

    레이어드 아키텍처 기반의 API 코드 설계 샘플
    user_controller.py
    from typing import Annotated
    
    from fastapi import APIRouter, Body, Depends
    from fastapi.security import OAuth2PasswordRequestForm
    
    router = APIRouter(prefix="/user")
    
    
    @router.post(path="", status_code=status.HTTP_201_CREATED)
    async def create_user(
        repo: dependency.UserRepo,
        body: Annotated[user_request.UserCreateRequest, Body()],
    ) -> ResponseEnum:
        """
        - one email can be used by only one user
        - username cannot be used if one is occupied
        - PW1 and PW2 mush be same
        """
        await user_process.create_user(repo=repo, data=body)
        return ResponseEnum.CREATE
    
    user_process.py
    async def create_user(
        *,
        repo: ports.UserRepository,
        data: user_request.UserCreateRequest,
    ) -> None:
        await verify_logic.verify_user_create(repo=repo, data=data)
        await user_logic.create_user(repo=repo, data=data)
    
    verify_logic.py
    async def verify_user_create(
        *,
        repo: ports.UserRepository,
        data: user_request.UserCreateRequest,
    ) -> None:
        user_list = await repo.read_user_by_name_email(name=data.name, email=data.email)
    
        username_conflict = [u for u in user_list if data.name == u.name]
        if username_conflict:
            raise NotUniqueError(field=data.name)
    
        email_conflict = [u for u in user_list if data.email == u.email]
        if email_conflict:
            raise NotUniqueError(field=data.email)
    
    user_logic.py
    async def create_user(
        *,
        repo: ports.UserRepository,
        data: user_request.UserCreateRequest,
    ) -> None:
        await repo.create_user(
            name=data.name,
            password=security.pwd_context.hash(secret=data.password1),
            email=data.email,
            created_datetime=datetime.now(configs.KST),
        )
    
  • 객체 지향적 프로그래밍 기반의 헥사고날 아키텍처(port - adapter 패턴) 적용을 통해 설계 유연성 확보

    헥사고날 아키텍처 DB CRUD 코드 샘플
    ports/user.py
    from abc import ABC, abstractmethod
    
    
    class UserRepository(ABC):
    
        @abstractmethod
        async def create_user(
            self,
            *,
            name: str,
            password: str,
            email: str,
            created_datetime: datetime,
        ) -> None: ...
    
    adapters/user.py
    class RdbUserRepository(UserRepository):
        def __init__(self, *, db: AsyncSession):
            self.db = db
    
        async def create_user(
            self,
            *,
            name: str,
            password: str,
            email: str,
            created_datetime: datetime,
        ) -> None:
            q = insert(UserEntity).values(
                name=name,
                password=password,
                email=email,
                created_datetime=created_datetime,
            )
            await self.db.execute(statement=q)
            await self.db.commit()
    
  • 배열 데이터의 일괄 처리 시 가독성 향상을 위해 comprehension 문법을 사용한 함수형 프로그래밍 적용

  • bcrypt 알고리즘을 사용한 JWT 로그인 기능
  • 서버 상태 측정을 위한 Prometheus, API 호출과 처리 결과 로깅을 위한 ELK 스택 적용
  • 배포를 위한 docker 컨테이너화

아키텍처

시스템 아키텍처

fastapi_board_architecture

  • 서버 가용성 확보를 위한 비동기 처리 적용
  • 커넥션 풀(connection pool) 기반의 ORM 사용
    • SQL injection 방지
    • 데이터베이스 부하 방지
  • 데이터베이스 부하를 줄이기 위한 캐시 서버(cache aside 패턴) 활용

DB 설계

---
config:
    theme: 'neutral'
---
erDiagram
    ROLE {
        bigint id   PK
        string name UK
    }

    STATE {
        bigint id   PK
        string name UK
    }

    ROLE |o..o{ USER : ""
    USER {
        bigint      id                  PK
        string      name                UK  "null"
        string      password                "null"
        string      email               UK  "null"
        datetime    created_datetime
        bigint      role_id             FK  "null"
    }

    STATE ||..o{ USER_STATE : ""
    USER ||..o{ USER_STATE : ""
    USER_STATE {
        bigint      user_id             PK, FK
        bigint      state_id            PK, FK
        string      detail                      "null"
        datetime    created_datetime
    }

    USER ||..o{ LOGGED_IN : create
    LOGGED_IN {
        bigint      id                  PK
        bigint      user_id             FK
        datetime    created_datetime
    }

    USER ||..o{ VOTER_COMMENT : vote
    COMMENT ||..o{ VOTER_COMMENT : voted
    VOTER_COMMENT {
        bigint user_id      PK, FK
        bigint comment_id   PK, FK
    }

    USER ||..o{ COMMENT : creates
    COMMENT {
        bigint      id                  PK
        bigint      user_id             FK
        bigint      post_id             FK
        datetime    created_datetime
        bool        is_active               "default=True"
    }

    COMMENT ||..|{ COMMENT_CONTENT : meta-data
    COMMENT_CONTENT {
        bigint id PK
        int version "default=0"
        datetime created_datetime
        text content
        bigint comment_id FK
    }

    CATEGORY |o..o{ CATEGORY : child
    CATEGORY {
        bigint      id          PK
        int         tier
        string      name
        bigint      parent_id   FK  "null"
    }

    USER ||..o{ VOTER_POST : vote
    POST ||..o{ VOTER_POST : voted
    VOTER_POST {
        bigint user_id PK, FK
        bigint post_id PK, FK
    }

    USER ||..o{ POST : create
    CATEGORY ||..o{ POST : categorize
    POST {
        bigint      id                  PK
        bigint      user_id             FK
        bigint      category_id         FK
        datetime    created_datetime
        bool        is_active               "default=True"
    }

    POST ||..|{ POST_CONTENT : meta-data
    POST_CONTENT {
        bigint      id                  PK
        int         version                 "default=0"
        datetime    created_datetime
        string      title
        text        content
        bigint      post_id             FK
    }
  • 데이터의 생성 및 관리 단위에 따라 테이블 분리 및 정규화
    • 게시글과 댓글의 이력 관리를 위한 테이블 분리
  • N + 1 문제 방지를 위해 연관 관계(relationship mapping) 사용 지양 및 native 쿼리 사용

    게시글 리스트 추출 Query
    SELECT
        p.id,
        p.created_datetime,
        pc.created_datetime AS updated_datetime,
        c.name AS category,
        u.name AS `user`,
        pc.title,
        pc.content,
        comment.comment,
        vote.vote
    FROM post AS p
    JOIN category c ON p.category_id = c.id
    JOIN `user` u ON p.user_id = u.id
    JOIN (
        SELECT
            t1.id,
            t1.created_datetime,
            t1.title,
            t1.content,
            t1.post_id
        FROM post_content AS t1
        JOIN (
            SELECT post_id, MAX(version) AS max_version
            FROM post_content
            GROUP BY post_id
            ) AS t2 ON t1.post_id = t2.post_id AND t1.version = t2.max_version
    ) AS pc ON p.id = pc.post_id
    LEFT JOIN (
        SELECT c.post_id, COUNT(*) AS comment
        FROM comment c
        WHERE
            c.is_active = TRUE
        GROUP BY post_id
    ) AS comment ON p.id = comment.post_id
    LEFT JOIN (
        SELECT
            voter_post.post_id,
            count(*) AS vote
        FROM voter_post
        GROUP BY voter_post.post_id
    ) AS vote ON p.id = vote.post_id
    WHERE
        p.is_active = TRUE
        AND (
            c.parent_id = :category_id
            OR c.id = :category_id
        )
        AND (
            u.name LIKE :keyword
            OR title LIKE :keyword
            OR content LIKE :keyword
        )
    ORDER BY p.id DESC
    LIMIT :size OFFSET :page
    

캐시 패턴

DB 호출 빈도가 가장 높은 API에 Cache Aside 패턴을 활용한 캐싱 적용

  • 유저 정보 API
  • 게시글 내용 API
---
title: Cache Aside Pattern - Read Sequence
config:
    theme: 'neutral'
---
sequenceDiagram
    autonumber
    Client -) FastAPI: API request
    activate FastAPI
    alt if cached data
        FastAPI -) Cache Server: check data
        Cache Server --) FastAPI: response data
    else if not cached data
        FastAPI -) Database: query
        Database --) FastAPI: result
    end
    FastAPI --) Client: response
    deactivate FastAPI