4 minute read

개요

작은 FastAPI 애플리케이션에서는 모든 Path Operation을 하나의 main.py에 작성해도 큰 문제가 없습니다. 하지만 프로젝트의 기능이 많아지면 하나의 파일에 사용자, 상품, 인증 등 여러 기능이 뒤섞이게 되어 코드의 유지보수가 어려워집니다.

FastAPI에서는 APIRouter를 이용하여 관련된 Path Operation을 여러 Module로 분리하고, include_router()를 통해 하나의 FastAPI Application으로 조립할 수 있습니다.


1. Python Module과 Package

FastAPI 프로젝트를 여러 파일로 분리하려면 먼저 Python의 Module과 Package를 이해할 필요가 있습니다.

Module

Module은 Python 코드가 작성된 하나의 .py 파일입니다. 아래의 Python 파일들은 각각 하나의 Python Module입니다.

main.py
users.py
items.py

Package

Package는 여러 Module을 계층적으로 구성하기 위한 구조입니다. 예를 들어 다음과 같은 프로젝트가 있다고 가정해 보겠습니다.

app/
├── __init__.py
├── main.py
└── routers/
    ├── __init__.py
    ├── users.py
    └── items.py

이를 Python의 Package/Module 관점에서 보면 다음과 같습니다.

app                 → Package
app.main            → Module

app.routers         → Subpackage
app.routers.users   → Module
app.routers.items   → Module

__init__.py

__init__.py는 일반적인 Python Package를 구성할 때 사용하는 특별한 파일입니다. Package가 Import될 때 실행할 초기화 코드를 작성하거나 Package 외부에 공개할 기능을 정리하는 데 사용할 수 잇으며, 별도의 동작이 필요하지 않다면 빈 파일로 두어도 됩니다.

Python 3.3 이후에는 Namespace Package가 지원되기 때문에 모든 Package에 반드시 __init__.py가 필요한 것은 아니지만, 일반적인 애플리케이션에서는 Package 구조를 명확하게 하기 위해 흔히 사용합니다.


2. APIRouter

APIRouter는 관련된 Path Operation을 하나의 그룹으로 관리하기 위한 FastAPI의 Router 클래스입니다. 기존에는 다음과 같이 FastAPI Application에 직접 Path Operation을 등록했습니다.

app = FastAPI()


@app.get("/users")
async def read_users():
    ...

여러 파일로 분리할 경우 각 Module에서는 APIRouter를 생성하여 Path Operation을 등록할 수 있습니다.

from fastapi import APIRouter

router = APIRouter()


@router.get("/users")
async def read_users():
    ...

APIRouter는 별도의 Web Server나 독립적인 FastAPI Application을 만드는 것이 아니며, 아래와 같이 여러 API를 기능별로 분리하여 관리하기 위한 구조라고 이해할 수 있습니다.

users.router ─┐
              ├──→ 하나의 FastAPI Application
items.router ─┘

3. include_router()

각 Module에서 생성한 APIRouter는 메인 FastAPI Application에 등록해야 실제 API로 사용할 수 있습니다. 이때 include_router()를 사용합니다.

from fastapi import FastAPI

from .routers import items, users

app = FastAPI()

app.include_router(users.router)
app.include_router(items.router)

전체적인 구조는 다음과 같습니다.

users.py
   ↓
users.router ───┐

                ├──→ main.py
                │     ↓
items.router ───┘   FastAPI app
   ↑
items.py

따라서 프로젝트가 커지면 main.py의 역할도 달라집니다.

작은 프로젝트

main.py
→ 대부분의 API를 직접 구현


큰 프로젝트

main.py
→ FastAPI Application 생성
→ 분리된 Router 연결
→ Application 구성 요소 조립

즉, main.py를 모든 기능을 구현하는 파일이 아니라 애플리케이션을 조립하는 중심 파일로 사용할 수 있습니다.


4. Router의 prefix

같은 기능에 속하는 API는 URL의 앞부분이 반복되는 경우가 많습니다. 예를 들어 Item API라면 다음과 같이 구성될 수 있습니다.

/items/
/items/{item_id}
/items/search

이때 APIRouter에 공통 prefix를 설정할 수 있습니다.

router = APIRouter(
    prefix="/items",
)

각 Path Operatino에서는 공통 부분을 제외하고 선언합니다.

@router.get("/")
async def read_items():
    ...


@router.get("/{item_id}")
async def read_item(item_id: int):
    ...

FastAPI는 이를 조합하여 최종 Path를 구성합니다.

Router prefix       Path Operation

/items        +     /
                      ↓
                   /items/

/items        +     /{item_id}
                      ↓
                   /items/{item_id}

이를 통해 같은 Router에서 반복되는 URL을 줄일 수 있습니다.


5. tags와 dependencies

APIRouter에는 prefix 외에도 Router에 속한 Path Operation들이 공유할 설정을 선언할 수 있습니다.

tags

router = APIRouter(
    prefix="/items",
    tags=["items"],
)

tags는 OpenAPI와 Swagger UI 등의 API 문서에서 관련 API를 하나의 그룹으로 분류하기 위한 Metadat입니다. 실제 URL 처리 방식에는 영향을 주지 않습니다.

dependencies

Router에 공통 Dependency를 지정할 수도 있습니다.

router = APIRouter(
    prefix="/items",
    dependencies=[Depends(verify_token)],
)

이 경우 해당 Router에 속하는 모든 Path Operation에서 verify_token Dependency가 실행됩니다.

/items/
/items/{item_id}
/items/search
       │
       └── 모두 verify_token 적용

따라서 여러 API에 동일한 인증이나 권한 확인이 필요하다면 각 Endpoint에서 Depends()를 반복하지 않고 Router 단위로 적용할 수 있습니다.


6. Relative Import

Package 내부의 다른 Module을 Import할 때 다음과 같은 표현을 사용할 수 있습니다.

from .routers import users

.은 현재 Package를 기준으로 하는 Relative Import를 의미합니다.

.   → 현재 Package
..  → 부모 Package

예를 들어 다음 구조에서

app/
├── dependencies.py
└── routers/
    └── items.py

itmes.py에서

from ..dependencies import get_token

이라고 하면 부모 Package인 app으로 이동하여 dependencies Module을 Import 하는 의미가 됩니다.


7. 실습 - Router 분리하기

다음과 같이 프로젝트를 구성해 보도록 하겠습니다.

app/
├── __init__.py
├── main.py
└── routers/
    ├── __init__.py
    ├── users.py
    └── items.py

routers/users.py

from fastapi import APIRouter

router = APIRouter(
    prefix="/users",
    tags=["users"],
)


@router.get("/")
async def read_users():
    return [
        {"id": 1, "name": "Alice"},
        {"id": 2, "name": "Bob"},
    ]


@router.get("/{user_id}")
async def read_user(user_id: int):
    return {"id": user_id}

routers/items.py

from fastapi import APIRouter

router = APIRouter(
    prefix="/items",
    tags=["items"],
)


@router.get("/")
async def read_items():
    return [
        {"id": 1, "name": "Keyboard"},
        {"id": 2, "name": "Mouse"},
    ]


@router.get("/{item_id}")
async def read_item(item_id: int):
    return {"id": item_id}

main.py

from fastapi import FastAPI

from .routers import items, users

app = FastAPI()

app.include_router(users.router)
app.include_router(items.router)


@app.get("/")
async def root():
    return {"message": "Hello FastAPI"}

실행 후 다음 Endpoint를 확인해 봅니다.

  • “GET /users/”

    아래 이미지를 보면 users.py 코드에 있는 read_users()가 호출되어 return 하도록 한 값들이 출력되는 것을 확인할 수 있습니다.


  • “GET /users/1”

    아래 이미지를 보면 users.py 코드에 있는 read_user()가 호출되어 Path Parameter의 값이 그대로 return되어 출력되는 것을 확인할 수 있습니다.


  • “GET /items/”

    아래 이미지를 보면 items.py 코드에 있는 read_items()가 호출되어 return 하도록 한 값들이 출력되는 것을 확인할 수 있습니다.


  • “GET /items/1”

    아래 이미지를 보면 items.py 코드에 있는 read_item()가 호출되어 Path Parameter의 값이 그대로 return되어 출력되는 것을 확인할 수 있습니다.


또한 /docs에서는 tags 설정에 따라 users, items API가 각각 그룹화되어 표시되는 것을 확인할 수 있습니다.



8. 정리

FastAPI 프로젝트가 커지면 하나의 main.py에 모든 Path Operation을 작성하기 보단 APIRouter를 이용하여 기능별로 API를 분리할 수 있습니다. 핵심 개념들은 다음과 같습니다.

Module
→ 하나의 .py 파일

Package
→ 여러 Module을 계층적으로 구성하는 구조

__init__.py
→ 일반적인 Python Package의 초기화 및 Package 구성에 사용하는 파일

APIRouter
→ 관련 Path Operation을 하나의 그룹으로 구성

include_router()
→ 분리된 Router를 FastAPI Application에 등록

prefix
→ Router의 모든 Path에 적용되는 공통 URL Prefix

tags
→ API 문서에서 관련 Endpoint를 그룹화하는 Metadata

dependencies
→ Router의 모든 Path Operation에 적용할 공통 Dependency

전체적인 프로젝트 구조는 다음과 같이 이해할 수 있다.

routers/users.py
→ User 관련 API

routers/items.py
→ Item 관련 API

        ↓

main.py
→ FastAPI Application 생성
→ 각각의 Router 등록

        ↓

하나의 FastAPI Application

APIRouter를 이용한 여러 파일 구조의 핵심은 단순히 파일 개수를 늘리는 것이 아니라 기능별 책임을 분리하고, 분리된 구성 요소를 하나의 Application으로 조립하여 프로젝트의 유지보수성을 높이는 것입니다.

참조

Comments