1. Domain Modeling
๊ฐ. Domain Model
1) ๋๋ฉ์ธ ๋ชจ๋ธ ๊ธฐ๋ณธ ์ดํด
โข
Domain Model: ๋น์ง๋์ค ๋ฌธ์ ํด๊ฒฐ์ ์ํด ๋น์ง๋์ค ์ ๋ฌธ๊ฐ๋ค์ด ๊ณ ์ํ ๋ฌธ์ ํด๊ฒฐ ์ ์ฐจ
โฆ
Domain: ํด๊ฒฐํ๋ ค๋ ๋น์ง๋์ค ๋ฌธ์
โฆ
Model: ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ์ ์ฐจ
โข
๋น์ง๋์ค ์ ๋ฌธ๊ฐ๋ค๊ณผ ์ํต ์, ์ฃผ์ํ ์๋ฏธ๊ฐ ํจ์ถ๋ ๋น์ง๋์ค ์ฉ์ด์ ๋ํ ์ดํด ํ์
โข
domain model์ ๋ํ ์ถฉ๋ถํ ์ดํด๋ฅผ ์ํด ์ฌ๋ก ์ค์ฌ์ผ๋ก ์ดํด ํ์
2) ํ ์คํธ ์ผ์ด์ค ์์ฑ๋ฒ
โข
domain model์ ๋ํ ์ฌ๋ก๋ฅผ ๋ฐํ์ผ๋ก ํ
์คํธ ์ผ์ด์ค ์์ฑ
โ business ์ฉ์ด๋ฅผ ๋ณ์๋ ํจ์์ ์ด๋ฆ์ผ๋ก ์ฐจ์ฉ
ex) 20๊ฐ ์๋์ Batch์ ๋ํด 2๊ฐ ์๋์ด ํฌํจ๋ Orderline์ด ํ ๋น๋๋ฉด Batch ๋ด ์๋์ 18๊ฐ๊ฐ ๋๋ค
def test_allocating_to_a_batch_reduces_the_available_quantity():
batch = Batch("batch-001", "SMALL-TABLE", qty=20, eta=date.today())
line = OrderLine("order-ref", "SMALL-TABLE", 2)
batch.allocate(line)
assert batch.available_quantity == 18
Python
๋ณต์ฌ
๋. ํ ์คํธ ๊ธฐ๋ฐ ๋๋ฉ์ธ ๋ชจ๋ธ๋ง ์ ์ฐจ
1) ํน์ ๋น์ง๋์ค ์ฌ๋ก ๋ง์กฑ ํ ์คํธ ์์ฑ
โข
Batch์ OrderLine์ ํ ๋น ์ ์ฐจ์ ๋ํ ํ
์คํธ ์ผ์ด์ค ์์ฑ
def test_allocating_to_a_batch_reduces_the_available_quantity():
batch = Batch("batch-001", "SMALL-TABLE", qty=20, eta=date.today())
line = OrderLine("order-ref", "SMALL-TABLE", 2)
batch.allocate(line)
assert batch.available_quantity == 18
Python
๋ณต์ฌ
2) ๋๋ฉ์ธ ๋ชจ๋ธ๋ง
โข
OrderLine๊ณผ Batch ๊ฐ์ฒด ์์ฑ
โข
Batch class ๋ด allocate ํ๋์ ๋ํ ๋ฉ์๋ ์ถ๊ฐ
@dataclass(frozen=True)
class OrderLine:
orderid: str
sku: str
qty: int
class Batch:
def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
self.reference = ref
self.sku = sku
self.eta = eta
self.available_quantity = qty
def allocate(self, line: OrderLine):
self.available_quantity -= line.qty #(3)
Python
๋ณต์ฌ
3) ํ ์คํธ ์ฑ๊ณต
โข
1)์ ๋ช
์ํ ํ
์คํธ ์ผ์ด์ค ํตํ
4) ๋ ๋ค๋ฅธ ๋น์ง๋์ค ์ฌ๋ก ๋ง์กฑ ํ ์คํธ ์์ฑ
โข
Batch์ OrderLine ํ ๋น ์กฐ๊ฑด์ ๋ํ ๋น์ง๋์ค ์ฌ๋ก๋ฅผ ๋ง์กฑํ๋ ํ
์คํธ ์ผ์ด์ค ์ถ๊ฐ
def make_batch_and_line(sku, batch_qty, line_qty):
return (
Batch("batch-001", sku, batch_qty, eta=date.today()),
OrderLine("order-123", sku, line_qty),
)
def test_can_allocate_if_available_greater_than_required():
large_batch, small_line = make_batch_and_line("ELEGANT-LAMP", 20, 2)
assert large_batch.can_allocate(small_line)
def test_cannot_allocate_if_available_smaller_than_required():
small_batch, large_line = make_batch_and_line("ELEGANT-LAMP", 2, 20)
assert small_batch.can_allocate(large_line) is False
def test_can_allocate_if_available_equal_to_required():
batch, line = make_batch_and_line("ELEGANT-LAMP", 2, 2)
assert batch.can_allocate(line)
def test_cannot_allocate_if_skus_do_not_match():
batch = Batch("batch-001", "UNCOMFORTABLE-CHAIR", 100, eta=None)
different_sku_line = OrderLine("order-123", "EXPENSIVE-TOASTER", 10)
assert batch.can_allocate(different_sku_line) is False
Python
๋ณต์ฌ
5) ๋๋ฉ์ธ ๋ฆฌ๋ชจ๋ธ๋ง
โข
ํ ๋น ์กฐ๊ฑด์ ๋ํ ํ
์คํธ ์ผ์ด์ค๋ฅผ ์ฑ๊ณต์ํค๊ธฐ ์ํด ๊ด๋ จ ๋ฉ์๋ ์ถ๊ฐ ๋ฐ ๊ธฐ์กด ํ ๋น ๋ฉ์๋ ์์
class Batch:
def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
self.reference = ref
self.sku = sku
self.eta = eta
self.available_quantity = qty
def allocate(self, line: OrderLine):
if self.can_allocate(line):
self._allocations.add(line)
def can_allocate(self, line: OrderLine) -> bool:
return self.sku == line.sku and self.available_quantity >= line.qty
Python
๋ณต์ฌ
๋ค. Entity vs Value Object
1) Entity
โข
์ ์: ๊ธธ๊ฒ ์ ์ง๋๋ ์๋ณ์๋ฅผ ํตํด ๊ตฌ๋ถ๋๋ ๊ฐ์ฒด
โข
ํน์ง
โ Identity Equality: ์ ์ผ์ฑ์ ๋ณด์ฅ ๋ฐ๋ ์๋ณ์๋ก ๊ตฌ๋ถ๋จ
โข
์์
โ reference๊ฐ ๊ฐ์ผ๋ฉด ๊ฐ์ ๊ฐ์ฒด์
โ == ์ฐ์ฐ์๋ก ๊ฐ์ฒด ๋๋ฑ์ฑ์ ์ฐ์ฐํ๊ธฐ ์ํด reference ๊ฐ์ ๊ธฐ์ค์ผ๋ก __eq__ ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํจ
โ set๊ณผ dict์์ ๊ด๋ฆฌ๋๊ธฐ ์ํด reference ๊ฐ์ ๊ธฐ์ค์ผ๋ก __hash__ ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํจ(set, dict์์ ๊ด๋ฆฌ๋ ํ์๊ฐ ์๋ค๋ฉด ์ค๋ฒ๋ผ์ด๋ฉ ํ ํ์ ์์
class Batch:
def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
self.reference = ref
self.sku = sku
self.eta = eta
self.available_quantity = qty
def __eq__(self, other):
if not isinstance(other, Batch):
return False
return other.reference == self.reference
def __hash__(self):
return hash(self.reference)
Python
๋ณต์ฌ
2) Value Object
โข
์ ์: ๊ฐ์ฒด์ ๋จ์ผ ๊ฐ์ด๋ ๊ฐ์ ์กฐํฉ์ผ๋ก ๊ตฌ๋ถ๋๋ ๊ฐ์ฒด
โข
ํน์ง
โ Value Equality: ์ค๋ณต์ด ํ์ฉ๋๋ ๊ฐ ๋๋ ๊ฐ์ ์กฐํฉ์ผ๋ก ๊ตฌ๋ถ๋จ
โ Immutable: ๊ฐ ์์ฒด๋ก ๊ตฌ๋ถ๋๊ธฐ ๋๋ฌธ์ ๊ฐ์ ๋ถ๋ณ์ฑ์ด ๋ง์กฑ๋์ด์ผ ํจ
ref) ๊ฐ์ด ๋ถ๋ณํด์ผํ๋ ์ด์ ๋ set์ด๋ dict ์๋ฃ๊ตฌ์กฐ์์ value object๊ฐ ๊ด๋ฆฌ๋๊ธฐ ์ํด์๋ ๋ถ๋ณ์ hash๊ฐ์ด ํ์ํ๋ฐ, value object์ ํด์ฌ๊ฐ์ value ๊ธฐ๋ฐ์ผ๋ก ์์ฑ๋จ
โข
์์
โ orderid, sku, qty๋ผ๋ ๊ฐ์ ์กฐํฉ์ด ๊ฐ์ผ๋ฉด ๊ฐ์ ๊ฐ์ฒด์
โ @dataclass(frozen=True): ์ธ์คํด์ค ์์ฑ ํ ๊ฐ์ ๋ถ๋ณ์ฑ์ ๋ณด์ฅ ๋ฐ์
โ @dataclass(unsafe_hash=True): __hash__ ๋ฉ์๋๊ฐ ์ถ๊ฐ๋จ
@dataclass(frozen=True)
class OrderLine:
orderid: str
sku: str
qty: int
Python
๋ณต์ฌ
๋ผ. Domain Service Function
1) ๊ฐ์ฒด ์ง์ฐฉ ์ง์
โข
๊ฐ์ฒด ๋ด์์ ์ฒ๋ฆฌํ๊ธฐ ์ด๋ ค์ด ๋น์ง๋์ค ๋ชจ๋ธ์ด ์๋ค๊ณ ๋ฌด์กฐ๊ฑด ๊ด๋ จ ๊ฐ์ฒด๋ฅผ ์์ฑํ์ง ๋ง๊ณ ํจ์๋ก ์ฒ๋ฆฌ๊ฐ๋ฅํ์ง ๊ณ ๋ คํด๋ณผ ๊ฒ
โ ๋ชจ๋ ๊ฒ์ ๊ฐ์ฒด๋ก ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก ์ค๊ณํ๋ฉด ํ์ด์ฌ์ ์ฒ ํ๊ณผ ๋ง์ง ์๊ฒ ๋ณต์ก์ฑ์ด ๋์์ง ์ ์์
โ ํ์ด์ฌ์ ๊ฐ์ฒด์งํฅ ๋ฟ๋ง ์๋๋ผ ์ฌ๋ฌ ํ๋ก๊ทธ๋๋ฐ ํจ๋ฌ๋ค์์ ์ง์ํจ.
2) ์ฌ๋ก
โข
ํน์ Batch์ ๋ํ OrderLine ํ ๋น์ด ์๋๋ผ ๋ค์์ Batch์ ๋ํ OrderLine ํ ๋น์ ๋ํ ๋น์ง๋์ค ์ฌ๋ก์ ๋ํ ๊ตฌํ์ ๊ธฐ์กด ๊ฐ์ฒด(Batch, OrderLine)๋ฅผ ํ์ฉํ ํจ์๋ก ์์
๊ฐ๋ฅ
def allocate(line: OrderLine, batches: List[Batch]) -> str:
try:
batch = next(b for b in sorted(batches) if b.can_allocate(line))
batch.allocate(line)
return batch.reference
except StopIteration:
raise OutOfStock(f"Out of stock for sku {line.sku}")
Python
๋ณต์ฌ
2. Repository Pattern ORM
๊ฐ. Repository vs Active Record
1) ์ค๊ณ ๋น๊ต
Repository: Domain Modeling ๋จ๊ณ์์ DB ๊ณ ๋ฏผ ๋ถํ์ / Active Record: ๋น์ง๋์ค ๋ก์ง ์ค๊ณ ๋จ๊ณ์์ DB ๊ณ ๋ฏผ ํ์
โข
Repository Pattern: ํํ๊ณ์ธต๊ณผ DB๊ณ์ธต์ด ๋ชจ๋ Domain Model์ ์์กดํจ
โฆ
Presentation Layer โ Domain Model โ Database Layer
โข
Active Record Pattern: ํํ๊ณ์ธต์ ๋น์ง๋์ค ๋ก์ง์ ์์กดํ๊ณ , ๋น์ง๋์ค ๋ก์ง์ DB๊ณ์ธต์ ์์กดํจ
โฆ
Presentation Layer โ Business Logic โ Database Layer
2) ์ฝ๋ ๋น๊ต
โข
Repository
โ domain model์ ๊ฐ์ ธ์ด(์์กดํจ , import model)
โ domain model์ ๊ธฐ์ค์ผ๋ก DB ์คํค๋ง ์์ฑ
โ start_mapper๋ฅผ ํธ์ถํ์ฌ domain ๋ชจ๋ธ๊ณผ ์คํค๋ง๋ฅผ ํ ๋๋ก DB ํ
์ด๋ธ ๋ณํ
from sqlalchemy.orm import mapper, relationship
import model
metadata = MetaData()
order_lines = Table(
"order_lines",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("sku", String(255)),
Column("qty", Integer, nullable=False),
Column("orderid", String(255)),
)
...
def start_mappers():
lines_mapper = mapper(model.OrderLine, order_lines)
Python
๋ณต์ฌ
โข
Active Record
โ DB ์คํค๋ง ์์ฑ == ๋๋ฉ์ธ ๋ชจ๋ธ ์์ฑ == DB ํ
์ด๋ธ ์์ฑ
class Order(models.Model):
pass
class OrderLine(models.Model):
sku = models.CharField(max_length=255)
qty = models.IntegerField()
order = models.ForeignKey(Order)
class Allocation(models.Model):
...
Python
๋ณต์ฌ
3) Trade Offs
๋จ์ CRUD ํํ์ ๊ฐ๋จํ ๋น์ง๋์ค ๋ก์ง์ ๋ค๋ฃจ๋ฉด Active Record, ๋ณต์กํ ๋น์ง๋์ค ๋ก์ง์ ๋ค๋ฃจ๋ฉด Repository๋ก ๊ตฌํ ๊ถ๊ณ
โข
์ฅ์
โฆ
Domain Model๊ณผ DB ๊ฐ ๋ถ๋ฆฌ์ ๋ฐ๋ผ Domain Model์ ๋ํ ๋จ์ํ
์คํธ ์ฉ์ด
โฆ
์ํํธ์จ์ด ์ค๊ณ ์ DB์ ๋ํด ๊ณ ๋ คํ์ง ์๊ณ Domain Model์ ์ง์คํ ์ ์์
โ Domain Model์ ๋ํด ๋จ์ํ
์คํธ๋ฅผ ์ถ๊ฐํจ์ผ๋ก์จ Domain Model ์ค์ฌ์ ์ํํธ์จ์ด ๊ฐ๋ฐ ๊ฐ๋ฅ
โข
๋จ์
โฆ
ORM mapping์ ๋ํ ์ถ๊ฐ์ ์ธ ์ฝ๋ ๋ฐ ์ ์ง๋ณด์ ๊ด๋ฆฌ ๋น์ฉ ๋ฐ์
๋. Repository Pattern
๋ค. Test
1) ORM
โข
classic mapper์ ์ ์๋์์ฌ๋ถ ํ์ธ
โข
์ฝ๋ ์ฐธ๊ณ
2) Repository
โข
์ฝ๋ ์ฐธ๊ณ
3. Abstraction
๊ฐ. Abstraction
1) ๋ชฉ์
โข
์์ ๊ฐ ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถฐ์ ํน์ ๊ตฌ์ฑ์์์ ๋ํ ์์ ์ด ๋ค๋ฅธ ๊ตฌ์ฑ์์์ ์ํฅ์ ์ฃผ์ง ์๋๋ก ํ๊ธฐ ์ํจ
๋. Abstraction ์ ์ฐจ
1) ์ฑ ์์ ๊ธฐ์ค์ผ๋ก ์ถ์ํ
โข
์ฑ
์: ๋
๋ฆฝ์ ์ผ๋ก ์คํ ๊ฐ๋ฅํ ๋ก์ง
โข
๊ฐ ์ฑ
์์ ๊ธฐ์ค์ผ๋ก ๊ฐ๋จํ ์ถ์ํ ๋ก์ง ๊ณ ๋ฏผ
โ ๊ตฌํ(HOW)๋ ๊ณ ๋ คํ์ง ์๊ณ ์ธํฐํ์ด์ค(WHAT)๋ง์ ๊ณ ๋ คํ ๊ฒ
2) ์ฑ ์ ์ค์ฌ์ผ๋ก ํ ์คํธ ์ผ์ด์ค ์์ฑ
โข
์ถ์ํ ๋ก์ง์ ๋ํ ๊ตฌํ๋ถ๋ฅผ ๊ณ ๋ คํ์ง ์๊ณ ์ถ์ํ ๋ก์ง์ ๋ํ ํ
์คํธ ์ผ์ด์ค ์์ฑ
def test_when_a_file_exists_in_the_source_but_not_the_destination():
source_hashes = {'hash1': 'fn1'}
dest_hashes = {}
expected_actions = [('COPY', '/src/fn1', '/dst/fn1')]
...
def test_when_a_file_has_been_renamed_in_the_source():
source_hashes = {'hash1': 'fn1'}
dest_hashes = {'hash1': 'fn2'}
expected_actions == [('MOVE', '/dst/fn2', '/dst/fn1')]
...
Python
๋ณต์ฌ
3) ํ ์คํธ ์ผ์ด์ค๋ฅผ ๋ง์กฑํ๋ ๊ธฐ๋ฅ ๊ตฌํ
โข
์ธํฐํ์ด์ค ๊ธฐ์ค์ผ๋ก ๊ตฌํ(HOW)์ ์ง์ค
Reference
โข
Harry Percival, Bob Gregory, Architecture Patterns with Python
โข
GitHub Repository of the book, https://github.com/cosmicpython/code/tree/master
โข
Florian Kromer, translated into FastAPI, , https://florian-kromer.medium.com/fastapi-microservice-patterns-domain-driven-design-e99f6f475691
โข
GitHub Repository in FastAPI, https://github.com/fkromer/code/tree/appendix_fastapi
โข
Cosmic Python, Book as Code, https://github.com/cosmicpython/book