포스트

API 서버 학습 : 기본 게시판

FastAPI로 만드는 게시판 프로젝트: 구현 과정과 앞으로의 확장 계획

기존에 AB180에서 인턴을 하며 Airflow와 Snowflake, Message Queue, AWS 등등 다양한 분야에 대해 Hands-on을 할 기회가 있었지만, 막상 API서버에 대한 작업은 적었다. 그래서 이번에는 기본 백엔드 API서버에 대해 학습을 해보기로 하며, 단순한 게시판을 FastAPI로 구현한 뒤, 천천히 하나씩 붙여나갈 계획이다.

최근 사이드 프로젝트로 FastAPI 기반의 게시판 API를 만들고 있으며, 이 프로젝트는 단순 CRUD를 넘어서 실제 서비스 수준의 구조와 품질을 목표로 하고 있다. 이 글에서는 지금까지 진행한 작업 내용과 기술적 설계, 그리고 앞으로 확장할 계획을 정리한다.

프로젝트 전체 구조

이번 프로젝트는 실제 운영 환경에도 바로 올릴 수 있는 구조를 목표로 설계했다.
현재의 구성은 다음과 같다:

FastAPI — API 서버
SQLAlchemy (Async) — 비동기 ORM
MySQL — 데이터베이스
JWT 인증 — 로그인/권한 관리
서비스 / 레이어드 아키텍처 — Repository / Service / Router 구조 분리

인증(Auth) 구현 과정

초기 버전에서는 FastAPI 기본 예제를 기반으로 로그인/회원가입 엔드포인트를 구성했다.

구현한 기능:

1
2
3
4
5
6
7
/auth/signup — 사용자 회원가입

/auth/login — OAuth2PasswordRequestForm 기반 로그인

JWT 발급 (access_token, token_type)

get_current_user 의존성을 통해 인증된 사용자 식별

중간에 몇 가지 흔한 오류(예: 422, JWT 파싱 문제)를 겪었지만, 스키마 구조 정리 후 안정적으로 동작하게 정리했다.

게시글(Post) API 설계

구현된 기능:

1
2
3
4
5
6
7
게시글 생성

게시글 단건 조회

게시글 목록 조회

게시글 삭제 (권한 체크 포함)

서비스 레이어를 활용해 아래처럼 책임을 분리했다:

Router → 요청/응답, HTTP 레이어

Service → 비즈니스 로직

Repository → DB 접근

이 구조 덕분에 테스트 작성이 쉬워졌고, 유지보수성도 높아졌다.

댓글(Comment) 기능 추가

게시판 서비스에서 필수적인 기능이기에 초기 단계에서 바로 댓글 기능도 구현했다.

1
2
3
4
5
댓글 생성

특정 게시글에 대한 댓글 조회(옵션)

댓글 삭제

댓글도 Post와 동일하게 Repository + Service + Router 레이어로 구조화했다.

의존성 주입(DI) 구조 정리

FastAPI의 Depends를 적극적으로 활용해 다음과 같은 DI 구조를 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class DependencyService:
    @staticmethod
    def get_user_service(db: AsyncSession) -> UserService:
        user_repo = UserRepository(session=db)
        return UserService(repo=user_repo)

    @staticmethod
    def get_post_service(db: AsyncSession) -> PostService:
        post_repo = PostRepository(session=db)
        return PostService(repo=post_repo)

    @staticmethod
    def get_comment_service(db: AsyncSession) -> CommentService:
        comment_repo = CommentRepository(session=db)
        return CommentService(repo=comment_repo)


def get_user_service(
    db: AsyncSession = Depends(get_db),
) -> UserServiceInterface:
    return DependencyService.get_user_service(db)


def get_post_service(
    db: AsyncSession = Depends(get_db),
) -> PostServiceInterface:
    return DependencyService.get_post_service(db)


def get_comment_service(
    db: AsyncSession = Depends(get_db),
) -> CommentServiceInterface:
    return DependencyService.get_comment_service(db)
1
2
3
4
5
6
7
8
9
10
11
@router.delete("/{post_id}", response_model=PostDeleteOut)
async def delete_post(
    post_id: int,
    post_service: PostService = Depends(get_post_service),
    current_user: User = Depends(get_current_user),
):
    user_id = current_user.id
    success = await post_service.delete_post(user_id=user_id, post_id=post_id)
    if not success:
        raise HTTPException(status_code=403, detail="Not authorized")
    return PostDeleteOut(success=True, message="Post deleted")

여기서 DB 세션(Async Session)이 한 번 열리고, 해당 요청의 모든 리포지토리/서비스가 동일 세션을 공유하도록 설계했다.

또한 async_sessionmaker 기반으로 세션을 관리하여 누수 없이 clean하게 한 요청을 마칠 수 있도록 했다.

blog

클라이언트는 가볍게 lovable을 써서 만들었다. 프론트쪽은 단순히 볼 수 있는 것 이외에 큰 의미를 두고 있지 않아 가볍게 만들고자 했다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.