게시판 서비스 FastAPI 벡엔드
개요¶
- FastAPI 기반 비동기 백엔드 서버
기술 스택¶
서비스 설명¶
-
절차적 프로그래밍을 기반으로 한 도메인 주도의 레이어드 아키텍처 구성을 통해 코드 가독성 확보
레이어드 아키텍처 기반의 API 코드 설계 샘플
user_controller.pyfrom 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.CREATEuser_process.pyasync 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.pyasync 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) -
객체 지향적 프로그래밍 기반의 헥사고날 아키텍처(port - adapter 패턴) 적용을 통해 설계 유연성 확보
헥사고날 아키텍처 DB CRUD 코드 샘플
ports/user.pyfrom 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.pyclass 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 컨테이너화
아키텍처¶
시스템 아키텍처¶
- 서버 가용성 확보를 위한 비동기 처리 적용
- 커넥션 풀(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
