파이썬과 DDD로 라이프 캘린더 만들기 #7 HTTP 공개 (1부 끝)

이전 글에서는 기능을 만들고 이를 DI 컨테이너로 관리하는 작업까지 진행했습니다. 우리의 기능이 완성된 것은 사실이지만, 서버 앱으로서 기능을 사용하는 일반적인 방법은 HTTP 호출일 것입니다. 이번 글에는 완성된 '캘린더 생성 기능'을 인기 있는 프레임워크인 FastAPI와 함께 HTTP로 공개하는 작업을 진행해 보겠습니다. 개인적으로는 이 시리즈가 시작한지 장장 7번째 글 만에 프레임워크를 설치하고 있다는 것도 재미있는 점이라고 생각합니다!

FastAPI 앱 생성

다음 명령어로 FastAPI와 Uvicorn을 설치하고 커밋합니다.

poetry add fastapi uvicorn

src/http/main.py 경로에 파일을 만들고, 다음과 같이 앱을 생성해 줍시다.

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return dict(path="/", status="ok")

다음 명령어로 FastAPI 앱의 개발 서버를 실행하고,

uvicorn src.http.main:app --reload

브라우저를 열어 8000번 포트로 접속해 봅시다. 다음과 같은 화면을 확인했다면 성공입니다!

HTTP API의 경우 /api/v1 처럼 prefix를 붙이는 경우가 많습니다. 이를 위해서 src/http/v1/router.py 경로에 파일을 만들고 다음과 같이 선언해 줍니다.

from fastapi import APIRouter

api_router_v1 = APIRouter(prefix="/v1")


@api_router_v1.get("/")
async def api_root():
    return dict(path="/api/v1", status="ok")

그리고 src/http/main.py 파일은 다음과 같이 수정해 줍니다.

from fastapi import FastAPI

from src.http.v1.router import api_router_v1

app = FastAPI()


@app.get("/")
async def root():
    return dict(path="/", status="ok")


api = FastAPI()  # for /api prefix
api.include_router(api_router_v1)
app.mount("/api", api)

다시 브라우저를 열고 http://localhost:8000/api/v1 경로로 접속했을 때 다음과 같은 화면이 나온다면 성공입니다! 그리고 여기서 커밋합니다!

캘린더 생성 기능을 HTTP로 공개

먼저 캘린더 생성 기능을 위한 HTTP 핸들러가 필요합니다. 이를 위한 라우터를 src/http/v1/calendar.py 경로에 생성합시다.

from fastapi import APIRouter

api_router_calendar = APIRouter(prefix="/calendar")


@api_router_calendar.post("/create")
async def create():
	pass

생성한 캘린더 라우터를 v1 라우터에 포함시켜 줍니다.

from fastapi import APIRouter

from src.http.v1.calendar import api_router_calendar

api_router_v1 = APIRouter(prefix="/v1")
api_router_v1.include_router(api_router_calendar)  # 추가!


@api_router_v1.get("/")
async def api_root():
    return dict(path="/api/v1", status="ok")

생성한 HTTP 핸들러에서 컨테이너를 사용할 수 있으려면, HTTP 핸들러가 실행되기 전에 Container.compose 함수를 실행해서 모든 객체를 미리 구성해둘 필요가 있습니다. 이를 위해서 다음과 같이 src/appl/container.py 파일에 내용을 추가해줍시다.

from src.appl.create_calendar import CreateCalendar
from src.appl.i_container import IContainer
from src.infra.repo.sa import SA
from src.infra.repo.sa_calendar_repo import SACalendarRepo
from src.infra.repo.sa_context import SAContext


class Container(IContainer):
    def compose(self) -> None:
        # repository
        self.register(SA("postgresql://qodot@localhost/lifecalendar", {}))
        self.register(SAContext(self.resolve(SA)))
        self.register(SACalendarRepo(self.resolve(SA)))

        # application
        self.register(
            CreateCalendar(self.resolve(SAContext), self.resolve(SACalendarRepo))
        )


container = Container()  # 추가!


def compose_container():  # 추가!
    global container
    container.compose()

그리고 src/http/main.py 파일을 열어 compose_container 함수를 실행해줍니다. 추가로 도메인 모델과 스키마를 매핑하기 위한 map_between_model_and_schema 함수도 호출해 줍니다(test_create_calendar.py 파일에서 테스트를 실행하기 위해 했던 사전 작업과 정확히 동일합니다)

from fastapi import FastAPI

from src.appl.container import compose_container
from src.http.v1.router import api_router_v1
from src.infra.db.mapper import map_between_model_and_schema

map_between_model_and_schema()  # 추가!
compose_container()  # 추가!
app = FastAPI()


@app.get("/")
async def root():
    return dict(path="/", status="ok")


api = FastAPI()  # for /api prefix
api.include_router(api_router_v1)
app.mount("/api", api)

우선 여기까지 커밋하겠습니다. 그리고 이제 src/http/v1/calendar.py 파일을 열고 컨테이너를 통해 CreateCalendar 객체를 사용해줍시다.

import datetime

from fastapi import APIRouter
from pydantic import BaseModel

from src.appl.container import container
from src.appl.create_calendar import CreateCalendar

api_router_calendar = APIRouter(prefix="/calendar")


class CreateCalendarReq(BaseModel):
    name: str
    birthday: datetime.date
    lifespan: int


@api_router_calendar.post("/create")
async def create(req: CreateCalendarReq):
    container.resolve(CreateCalendar).run(req.name, req.birthday, req.lifespan)

여기서 커밋하겠습니다! FastAPI의 가장 멋진 점은 이렇게만 개발해도 HTTP 인터페이스에 대한 스웨거 문서를 만들어준다는 점입니다. http://localhost:8000/api/docs로 접속하면 다음과 같은 화면을 확인할 수 있을 것입니다.

HTTP 인터페이스의 요청과 응답의 모든 스펙을 확인하고 테스트 해볼 수 있습니다. Try it out 버튼을 눌러 API를 실행해서 실제로 DB에 row가 잘 쌓이는지 확인해 보세요!

작성한 HTTP 핸들러의 URL 경로가 흔히 이야기하는 RESTful 규칙을 따르고 있지 않습니다. 이에 대해서는 또 이야기 할 기회가 있을 것 같네요!

이제 진짜로 모든 것이 완성되었습니다! 지금까지 함께한 작업들은 다음과 같을텐데요,

  1. 기획의 맥락을 파악 후에 용어와 요구사항을 정의하고,
  2. 모델을 작성하고,
  3. 데이터베이스를 설정하고,
  4. 레파지토리를 작성하고,
  5. 통합 기능을 완성하고,
  6. 복잡한 객체 생성 과정을 위임하고,
  7. 이를 HTTP로 공개했습니다.

이렇게 하나의 기능을 추가하기 위한 기본적인 코드 작성 사이클이 종료된 것입니다. 물론 우리의 최종 목적인 '라이프 캘린더'를 완성하기 위한 여정은 아직도 한참 남아있기 때문에 시리즈는 계속 이어질 것입니다만, DDD의 구체적인 패턴이라고 할 수 있는 Building Blocks를 파이썬에서 나름대로 구현하려고 했던 저의 고민들 중 기본적인 것들은 일단 여기까지입니다. 도움이 되셨으면 좋겠네요!

아직도 흥미가 사라지지 않은 분들이 계시다면 다음 시리즈에서 뵙겠습니다. 리팩토링도 하고, 새로운 기능도 추가할 예정이니까요!