Search
๐Ÿ“˜

Architecture Patterns with Python - Harry Percival, Bob Gregory

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
โ€ข
โ€ข
โ€ข
Cosmic Python, Book as Code, https://github.com/cosmicpython/book