대부분 개발자는 도메인 모델을 본 적이 없으며 데이터 모델만 봤을 것이다
-Cyrille Martraire, DDD EU 2017
새로운 시스템을 도입할 때, DB모델링에 바로 착수해왔다.
하지만 여기서부터 모든 것이 잘못되는 지점이다.
유저나 비즈니스 관계자들은 DB 스키마에 관심이 없다.
그들은 어떤 행동(액션)과 그 결과에 관심을 갖는다.
따라서 도메인을 분석할 때는 "행동"에 집중해야 한다.
위 정의보다 "당신의 소프트웨어가 존재하는 이유"라는 간접적인 표현이 더 와닿는다.
유관부서(비즈니스 전문가들)과 대화를 통해 도메인 모델을 이해해야 한다.
도메인 모델에 사용할 용어와 규칙을 정해야 한다.
얼리버드
서비스의 콘텍스트 다이어그램
얼리버트에 대한 노트
매치(Match)
는 단일 경기 하나를 의미한다.
ex) 10월 10일 용산 아이파크몰 (2구장/맨유) 오전 10시 매치매치는 매달 1일 00시에 공개된다.
매치에는 여러 개의매치 신청(MatchApply)
를 할 수가 있다.
ex) 매치에 11명 지원 or 매치에 5명 지원매치 시작 시간에 따라 매치 신청 가격을 차등 부여한다.
1) 매치 공개 후, 매치일이 7일 이하 남은 평일 매치의 경우(if)
- 8시간 이전: 3000원 할인
- 7시간 이전: 2000원 할인
- 6시간 이전: 1000원 할인
2) 매치 공개 후, 매치일이 7일 초과 남은 평일 매치의 경우(else)
- 7일 이전: 7000원 할인
- 6일 이전: 6000원 할인
- 5일 이전: 5000원 할인
- 4일 이전: 4000원 할인
- 3일 이전: 3000원 할인
class EarlyBird:
"""얼리버드 시스템"""
def __init__(self, stadium: str):
self.stadium = stadium
self._earlybird_table = set()
def __eq__(self, other):
if not isinstance(other, EarlyBird):
return False
return other.stadium == self.stadium
def __hash__(self):
return hash(self.stadium)
def __gt__(self, other):
if self.stadium is None:
return False
if other.stadium is None:
return True
return self.stadium > other.stadium
def is_addable(self, timepriceline):
if timepriceline not in self._earlybird_table:
return True
else:
return False
def add_timepriceline(self, timepriceline):
if self.is_addable(timepriceline):
self._earlybird_table.add(timepriceline)
def get_earlybird_price(self, match_date):
price = 10000
try:
if self._earlybird_table:
for line in self._earlybird_table:
if match_date == line.time:
price = line.price
break
return price
except Exception as e:
raise ValueError(e)
from dataclasses import dataclass
from datetime import datetime
@dataclass(frozen=True)
class TimePriceLine:
"""할인 가능 시간 및 가격 정보"""
time: datetime
price: int
얼리버드 시스템에는 여러 라인이 원소로 있다.
각 라인은 시간과 가격정보를 갖고 있다.
간단히 YAML 파일로 작성해보면 아래와 같다.
Early_brid: 10
Time_price_line:
- time: datetime.datetime(2021, 10, 10, 10, 0, tzinfo=<UTC>)
price: 8000
- time: datetime.datetime(2021, 10, 10, 12, 0, tzinfo=<UTC>)
price: 8000
- time: datetime.datetime(2021, 10, 10, 14, 0, tzinfo=<UTC>)
price: 7000
@dataclass(frozen=True)는
해당 클래스의 모든 속성 정보들로 해시를 생성한다.
따라서, 객체 생성 후 인스턴스 변수를 저장하는 것이 불가능하다.(불변 객체)
e = Name("용산 2구장 맨체스터 유나이티드")
e.add_instance_variable = 1
>> Traceback (most recent call last):
File "main.py", line 20, in <module>
e.add_instance_variable = 1
File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'add_instance_variable'
얼리버드를 이루는 여러 라인들은 시간과 가격 정보 자체가 식별자가 된다.
값으로
두 객체의 동등성을 판별한다는 이야기다.
예를 들어, 내 지갑에 있는 1000원과 친구 지갑에 있는 1000원은 같다. 색이 좀 더 누렇게 바랬다고 해서 두 지폐가 다르게 사용되지 않는 것처럼 말이다.
1000원 지폐가 50,000원 지폐가 되면 전혀 다른 객체
가 되어 버린다.
하지만 지폐
라는 정체성은 유지가 된다.
용산 아이파크몰 2 경기장의 명칭은 맨유 구장이다.
맨체스터 유나이티드가 팀명을 변경하면, 마찬가지로 용산 2경기장 이름도 변경될 것이다. 그럼에도 그 경기장
이라는 인식(정체성)은 변하지 않는다.
from dataclasses import dataclass
@dataclass(frozen=True)
class Name:
name: str
class Stadium:
def __init__(self, name: Name):
self.name = name
yongsan_2 = Stadium(Name("용산 2구장 맨유"))
yongsan_1 = yongsan_2
yongsan_2.name = Name("용산 2구장 맨체스터 유나이티드")
assert yongsan_1 == yongsan_2
실제 서비스에서 필요한 행동은 운영팀에서 매달 1일 모든 매치들에
얼리버드 시스템을 일괄 적용 및 할당하는 것이다.
def allocate(line: TimePriceLine, earlybirds: List[EarlyBird]) -> str:
try:
earlybird = next(
e for e in sorted(earlybirds) if e.is_addable(line)
)
earlybird.add_timepriceline(line)
return earlybird.stadium
except StopIteration:
raise OutOfMatch(f"Out of match for {line.time, line.price}")