파이썬과 DDD로 라이프 캘린더 만들기 #8 캘린더 수정 기능
시리즈 전체 목록 (계속 업데이트 중)
우리는 1편부터 7편까지에 걸쳐 캘린더 생성 기능을 완성했습니다. 이번 글에서는 정리한 요구사항 중, 생성된 캘린더의 이름, 생년월일, 예상 수명은 언제든 수정할 수 있다.
에 해당하는 기능을 한 편에 걸쳐 빠르게 만들어 보겠습니다. 필요한 설정과 인프라 코드를 모두 만들어 두었기 때문에 금방 할 수 있을 겁니다!
모델 작성
우선 TestCalendar
에 다음과 같은 테스트 코드를 작성합시다.
import datetime
from src.domain.entity.calendar import Calendar
class TestCalendar:
...
def test_update_basic_info(self):
calendar = Calendar.create("고도", datetime.date(1988, 6, 21), 80)
name = "뉴고도"
birthday = datetime.date(2000, 6, 21)
lifespan = 100
calendar.update_basic_info(name, birthday, lifespan)
assert calendar.name == name
assert calendar.birthday == birthday
assert calendar.lifespan == lifespan
assert len(calendar.years) == lifespan + 1
assert len(calendar.years[0].weeks) == 52
테스트 실패를 확인하고 Calendar
엔티티에 경로에 다음과 같은 메소드를 작성해줍시다.
class Calendar:
...
def update_basic_info(
self, name: str, birthday: datetime.date, lifespan: int
) -> None:
self.name = name
self.birthday = birthday
self.lifespan = lifespan
테스트를 다시 실행하면 다음과 같은 에러를 볼 수 있을 것입니다.
> assert len(calendar.years) == lifespan + 1
E assert 81 == (100 + 1)
E + where 81 = len([<src.domain.entity.calendar.Year object at 0x1062c5dd0>, <src.domain.entity.calendar.Year object at 0x106323c90>, <sr... <src.domain.entity.calendar.Year object at 0x10632a490>, <src.domain.entity.calendar.Year object at 0x10632b210>, ...])
E + where [<src.domain.entity.calendar.Year object at 0x1062c5dd0>, <src.domain.entity.calendar.Year object at 0x106323c90>, <sr... <src.domain.entity.calendar.Year object at 0x10632a490>, <src.domain.entity.calendar.Year object at 0x10632b210>, ...] = <src.domain.entity.calendar.Calendar object at 0x106392e50>.y
update_basic_info
메소드에서 years
를 업데이트하지 않았기 때문이죠! create
메소드처럼 Year
의 리스트를 똑같이 업데이트할 필요가 있습니다. _update_years
비공개 메소드를 추가해서 해결하고 테스트가 성공하는 것을 확인합니다.
class Calendar:
...
def update_basic_info(
self, name: str, birthday: datetime.date, lifespan: int
) -> None:
self.name = name
self.birthday = birthday
self.lifespan = lifespan
self._update_years()
def _update_years(self) -> None:
self.years = [
Year(yearnum=yearnum)
for yearnum in range(
self.birthday.year, self.birthday.year + self.lifespan + 1
)
]
create
메소드에서도 _update_years
를 재사용해주고 모든 테스트가 통과하는지 확인해봅시다. 여기서 커밋합시다!
class Calendar:
...
@classmethod
def create(cls, name: str, birthday: datetime.date, lifespan: int) -> Calendar:
instance = cls(
id=uuid.uuid4(), name=name, birthday=birthday, lifespan=lifespan, years=[]
)
instance._update_years()
return instance
기능 작성
캘린더 생성과 동일한 느낌으로 UpdateCalendar
객체를 테스트와 함께 작성해 줍시다. 생성이 아니라 업데이트이기 때문에, 캘린더 모델의 ID가 필요하다는 점에 주의해 줍시다. 작성 후에는 DI 컨테이너에 등록도 같이 해줍니다.
# src/appl/update_calendar.py
class UpdateCalendar:
def __init__(self, db_context: IDBContext, calendar_repo: ICalendarRepo) -> None:
self.db_context = db_context
self.calendar_repo = calendar_repo
def run(
self, calendar_id: uuid.UUID, name: str, birthday: datetime.date, lifespan: int
) -> None:
with self.db_context.begin_tx():
calendar = self.calendar_repo.get_or_error(calendar_id)
calendar.update_basic_info(name, birthday, lifespan)
# src/appl/container.py
class Container(IContainer):
def compose(self) -> None:
...
self.register(
UpdateCalendar(self.resolve(SAContext), self.resolve(SACalendarRepo))
)
생성 테스트를 작성했을 때와는 다르게, 파이썬의 테스트 도구인 MagicMock
을 이용해서 테스트 코드 실행시 데이터베이스가 필요 없도록 설정해 줍시다.
# test/appl/test_update_calendar.py
class TestUpdateCalendar:
def test_run(self):
calendar = self._create_calendar()
db_context = MagicMock(spec=IDBContext)
calendar_repo = MagicMock(spec=ICalendarRepo)
calendar_repo.get_or_error.return_value = calendar
command = UpdateCalendar(db_context, calendar_repo)
name = "뉴고도"
birthday = datetime.date(2000, 6, 21)
lifespan = 100
command.run(calendar.id, name, birthday, lifespan)
assert calendar.name == name
assert calendar.birthday == birthday
assert calendar.lifespan == lifespan
def _create_calendar(self) -> Calendar:
return Calendar.create("고도", datetime.date(1988, 6, 21), 80)
테스트 코드에서 UpdateCalendar
객체를 DI 컨테이너를 통해서 가져오는 것이 아니라 직접 생성해주는 것을 볼 수 있습니다. 실제로는 진짜 동작하는 객체를 주입해야 하지만, 테스트 환경에서는 mock 객체를 주입해야하기 때문입니다. 나중에 이 프로젝트에 환경별로 설정을 따로 구성할 수 있게 만들고 나면, 컨테이너도 환경별로 따로 구성할 생각입니다. 그 때 까지는 이대로 두겠습니다. 테스트가 동작하는 것까지 확인하고 커밋해줍시다!
HTTP 공개
기능에 HTTP 핸들러를 연결하는 것은 캘린더 생성과 동일합니다. 캘린더 ID를 URL 경로에 표기해 주는 부분만 확인합시다. 스웨거에 들어가서 API를 호출해본 후에 커밋합니다!
# src/http/v1/calendar.py
class UpdateCalendarReq(BaseModel):
name: str
birthday: datetime.date
lifespan: int
@api_router_calendar.post("/{calendar_id}/update")
async def update(calendar_id: uuid.UUID, req: UpdateCalendarReq):
container.resolve(UpdateCalendar).run(
calendar_id, req.name, req.birthday, req.lifespan
)
미리 만들어둔 도구들을 이용해서 새 기능을 빠르게 완성해 보았습니다. 다음 글에서는 위에서 잠깐 언급했던, 우리 프로젝트를 환경별로 설정을 구성하고 설정에 맞게 DI 컨테이너 등의 동작을 구분할 수 있게 만들어 보겠습니다.
Member discussion