CampusMeet

[Backend] FastAPI

dong_seok 2024. 8. 14. 22:12

프로젝트의 백엔드를 FastAPI로 구성하기로 했지만, 실전에 들어가 보니 FastAPI가 상대적으로 덜 대중적이다 보니 자료를 찾기가 쉽지 않고, 디렉토리 구조를 어떻게 잡아야 할지 감이 잘 오지 않았습니다. 그래서 "https://github.com/fastapi/full-stack-fastapi-template"를 참고하여 디렉토리 구조를 작성해보았습니다. 오늘은 이 디렉토리 구조의 각 부분이 어떤 역할을 하는지 간단하게 정리하도록 하겠습니다.

1. 디렉토리 구조 시각화
2. 디렉토리 구조별 역할

1. 디렉토리 구조 시각화

디렉토리 구조를 작성했지만, 막상 만들어보니 헷갈리는 부분이 많고 한눈에 들어오지 않아서 구조를 한눈에 보면서 각 디렉토리별 역할을 정리해보기로 했습니다. 외부 사이트를 이용할까 했는데 인터넷을 찾아보니 mac에서는 'tree' 라는 녀석을 이용해서 매우 간편하게 시각화 할 수 있다는 것을 알 았습니다.

 

1) Install

brew install tree

 

위 명령어로 tree를 설치해줍니다.

 

2) Termenal

tree {시각화 하려는 디렉토리명}

 

이렇게 간단하게 명령어를 입력하면 디렉터리 구조를 한눈에 볼 수 있습니다.

 

2. 디렉토리 구조별 역할

위의 이미지를 보면 알 수 있듯이, 'app' 이라는 루트 디렉토리 안에 상당히 많은 하위 디렉토리가 포함되어 있습니다. 이제 이 디렉토리들이 각각 어떤 역할을 하는지 하나씩 살펴보겠습니다.

 

1) api

api 디렉토리는 FastAPI 애플리케이션의 엔드포인트(endpoints) 또는 라우트(routes)를 정의하는 코드가 포함된 디렉토리입니다. 클라이언트가 API 서버에 요청을 보낼 때, 해당 요청을 처리하는 로직이 이곳에 위치하게 됩니다. 

 

(1) deps.py

이 파일은 FastAPI의 의존성 주입(Dependency Injection)에서 사용되는 공통 함수들을 정의합니다. 주로 데이터베이스 세션을 생성하거나, 사용자 인증/인가 관련 기능을 처리하는 함수들을 포함합니다.

from sqlalchemy.orm import Session
from fastapi import Depends
from core.config import get_db

def get_db_session() -> Session:
    db = get_db()
    try:
        yield db
    finally:
        db.close()

def get_current_user(db: Session = Depends(get_db_session)):
    # 사용자 인증 로직 추가
    pass

 

(2) main.py

'api' 디렉토리 내에서 FastAPI 애플리케이션의 진입점 역할을 합니다. 여기서 'routes' 디렉토리 내에 있는 모든 라우터를 등록하고, 주요 설정을 적용할 수 있습니다.

from fastapi import FastAPI
from api.routes import board, user, comment

app = FastAPI()

app.include_router(board.router, prefix="/boards")
app.include_router(user.router, prefix="/users")

 

(3) routes

각각의 기능별로 API 엔드포인트를 정의하는 모듈이 위치합니다. 각 기능별 라우트를 파일로 분리하여 코드의 가독성을 높이고, 확장성 있는 구조를 만듭니다. 예를들어 게시판 관련 엔드포인트를 가진 'board.py'를 만든다고 하면 아래와 같이 작성할 수 있습니다.

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from typing import List

from models.board import Board
from crud.board import get_boards
from api.deps import get_db_session

router = APIRouter()

@router.get("/", response_model=List[Board])
def read_boards(skip: int = 0, limit: int = 10, db: Session = Depends(get_db_session)):
    boards = get_boards(db, skip=skip, limit=limit)
    return boards

 

(4) 구조적 이점

 

  • 모듈화: 'routes' 디렉토리로 라우터들을 분리하여 각 기능을 독립적으로 관리할 수 있습니다. 기능별로 API를 확장하기 쉽습니다.
  • 재사용성: 'deps.py' 에 의존성을 정의하여, 여러 엔드포인트에서 동일한 로직을 반복하지 않고 재사용할 수 있습니다.
  • 관리 용이성: 'api/main.py' 를 통해 API의 주요 설정과 라우터 등록을 중앙에서 관리할 수 있습니다. 프로젝트가 커져도 파일 구조가 깔끔하게 유지됩니다.
  • 확장성: 기능이 추가될 때마다 'routes' 디렉토리에 새로운 파일을 추가하고, 'api/main.py '에서 이를 등록하면 됩니다.

2) core

core 디렉토리는 FastAPI 프로젝트의 핵심 설정과 구성 요소를 관리하는 곳입니다. 이 디렉토리에는 애플리케이션 전역에서 사용되는 설정, 데이터베이스 연결, 보안 관련 설정 등을 관리하는 파일들이 포함됩니다. core 디렉토리는 프로젝트의 중요한 기능을 설정하고 초기화하는 역할을 합니다.

 

(1) config.py

config.py 파일은 애플리케이션의 전역 설정을 관리합니다. 이 파일에는 데이터베이스 연결 문자열, 환경 변수, API 키, 애플리케이션의 설정값들이 포함됩니다. 프로젝트 전반에 걸쳐 사용하는 설정 값을 이곳에서 관리함으로써, 코드의 가독성과 유지보수성을 높일 수 있습니다.

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from dotenv import load_dotenv

# .env 파일 로드
load_dotenv()

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_ignore_empty=True,
        extra="ignore",
    )

    POSTGRES_USER: str = Field(..., env='POSTGRES_USER')
    POSTGRES_PASSWORD: str = Field(..., env='POSTGRES_PASSWORD')
    POSTGRES_SERVER: str = Field(..., env='POSTGRES_SERVER')
    POSTGRES_PORT: str = Field(..., env='POSTGRES_PORT')
    POSTGRES_DB: str = Field(..., env='POSTGRES_DB')

    @property
    def SQLALCHEMY_DATABASE_URI(self) -> str:
        return (
            f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
            f"@{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
        )


settings = Settings()

 

이 설정들은 애플리케이션 전역에서 쉽게 접근할 수 있습니다. 예를 들어, 데이터베이스 연결 시 "settings.DATABASE_URL" 을 사용하면 됩니다.

 

(2) db.py

db.py 파일은 데이터베이스와의 연결을 관리합니다. SQLAlchemy와 같은 ORM(Object-Relational Mapping) 도구를 사용하여 데이터베이스와의 세션을 관리하고, 데이터베이스 모델을 초기화하거나 마이그레이션 작업을 수행할 수 있습니다.

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from core.config import settings

# 데이터베이스 엔진 생성
engine = create_engine(settings.DATABASE_URL)

# 세션 로컬 정의
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 기본 모델 클래스 정의
Base = declarative_base()

 

create_engine은 앞에서 만든 'DATABASE_URL' 을 받아 데이터베이스 엔진을 생성하여 데이터베이스와 연결을 설정하는데 사용됩니다. BASE 는 모든 모델이 상속받는 기본 클래스입니다. 이 클래스를 상속받아 SQLAlchemy 모델을 정의합니다. 

 

 

3) crud

crud 디렉토리는 FastAPI 프로젝트에서 데이터베이스와 상호작용하는 로직을 담당하는 곳입니다. 주로 SQLAlchemy ORM을 사용하여 데이터베이스 조작을 수행하는 함수들이 정의됩니다. 예를들어 게시판과 관련된 CRUD 작업을 하는 파일은 아래와 같이 작성할 수 있습니다.

from sqlalchemy.orm import Session
from models.board import Board
from schemas.board import BoardCreate

def create_board(db: Session, board: BoardCreate) -> Board:
    db_board = Board(**board.dict())  # BoardCreate 스키마에서 데이터를 Board 모델로 변환
    db.add(db_board)  # 데이터베이스 세션에 새 게시글 추가
    db.commit()  # 데이터베이스에 변경 사항 커밋
    db.refresh(db_board)  # 새 게시글 객체를 최신 상태로 갱신
    return db_board  # 새 게시글 객체 반환

def get_board(db: Session, board_id: int) -> Board:
    return db.query(Board).filter(Board.id == board_id).first()  # 주어진 ID로 게시글 조회

def get_boards(db: Session, skip: int = 0, limit: int = 10):
    return db.query(Board).offset(skip).limit(limit).all()  # 게시글 목록 조회 (페이징 처리)

def delete_board(db: Session, board_id: int):
    db_board = db.query(Board).filter(Board.id == board_id).first()  # 주어진 ID로 게시글 조회
    if db_board:
        db.delete(db_board)  # 게시글이 존재하면 삭제
        db.commit()  # 데이터베이스에 변경 사항 커밋
    return db_board  # 삭제된 게시글 객체를 반환하거나, 게시글이 없으면 None 반환

 

4) models

models 디렉토리는 FastAPI 프로젝트에서 데이터베이스 테이블과 객체를 매핑하는 SQLAlchemy 모델을 정의하는 곳입니다. 이 모델들은 데이터베이스의 테이블 구조를 코드로 표현하며, CRUD 작업에서 이 모델들을 사용하여 데이터베이스와 상호작용합니다. 만약 게시판과 관련된 데이터베이스 테이블을 정의한다고하면 아래와 같이 작성할 수 있습니다.

from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import relationship
from core.db import Base

class Board(Base):
    __tablename__ = "boards"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String(100), nullable=False)
    content = Column(Text, nullable=False)
    user_id = Column(Integer, ForeignKey("users.id"))

    user = relationship("User", back_populates="boards")
    comments = relationship("Comment", back_populates="board")

 

5) tests

tests 디렉토리는 FastAPI 프로젝트에서 유닛 테스트통합 테스트를 관리하는 곳입니다. 테스트는 애플리케이션의 기능이 올바르게 작동하는지 확인하고, 코드의 품질을 유지하며, 변경 사항이 기존 기능에 영향을 미치지 않도록 하는 데 중요합니다.

API 엔드포인트별로 테스트 파일을 나누고, 다양한 경로와 파라미터를 고려한 테스트 케이스를 작성합니다. 게시판 관련 간단한 테스트 코드를 작성하면 다음과 같습니다.

import pytest
from fastapi.testclient import TestClient
from main import app
from crud.board import create_board
from models.board import Board
from core.db import SessionLocal

client = TestClient(app)

@pytest.fixture
def db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

def test_create_board(db):
    response = client.post("/boards/", json={"title": "Test Board", "content": "Test Content"})
    assert response.status_code == 200
    assert response.json()["title"] == "Test Board"

def test_read_board(db):
    create_board(db, {"title": "Existing Board", "content": "Existing Content"})
    response = client.get("/boards/1")
    assert response.status_code == 200
    assert response.json()["title"] == "Existing Board"

 

테스트 전략

 

  • 테스트 환경 설정: 테스트가 원활하게 실행되도록 데이터베이스와 서버를 설정합니다. 보통 테스트용 데이터베이스를 사용하여 실제 데이터베이스를 보호합니다.
  • 테스트 데이터 관리: 테스트가 독립적으로 실행될 수 있도록 테스트 데이터를 생성하고 정리합니다.
  • 자동화: CI/CD 파이프라인에 통합하여, 코드 변경 시 자동으로 테스트가 실행되도록 설정합니다.

 

 

 

참고자료

https://github.com/fastapi/full-stack-fastapi-template