도메인 모델 객체가 개념적으로나 영속성 저장소 안에서나 내부적인 일관성을 유지하는 방법을 살펴볼 예정
불변조건, 제약, 일관성
도메인 모델은 시스템의 제약을 강제로 지키게 해서 시스템이 만족하는 불변조건을 유지하려는 목적으로 사용
- 불변조건 : 항상 참이어야 하는 조건
- 제약 : 모델이 취할 수 있는 상태의 수를 제한
작업이 끝나면 도메인 모델은 모든 불변조건을 만족하는 일관성있는 최종 상태로 끝난다는 사실 보장해야함
주문 라인은 한번에 한 배치에만 할당 될 수 있다 - 비즈니스
이 규칙은 불변조건을 만드는 비즈니스 규칙
불변조건, 동시성, 락
주문 라인 수량보다 더 작은 배치에 라인을 할당할 수는 없다 - 비즈니스
- 배치에 있는 재고보다 많은 재고를 주문 라인에 할당할 수 없음. 시스템 상태를 업데이트 할 때마다 가용 재고 수량이 0이상인지 확인해야함
- 동시성 적용시 재고를 여러 주문라인에 동시에 할당 할 수 있게 되며, 보통 테이블에 락을 적용해 문제를 해결
- 하지만 규모가 커질 수록 전체 batches 테이블의 각 행에 락을 추가할 수 없음. 교착상태가 되거나 성능에 문제가 발생 할 수 있다
애그리게이트란?
- 시스템의 불변조건을 보호하면서 동시성을 살리려면? 불변조건을 유지하려면 불가피한 동시 쓰기를 막아야함
- 여러 사용자가 A 제품을 동시에 할당할 수 있다면 과할당이 일어날 수 있지만, 서로 다른 제품은 할당해도 안전하다.
애그리게이트 패턴
- 다른 도메인 객체를 포함하여 객체 컬렉션 전체를 한꺼번에 다룰 수 있게 해주는 도메인 객체
- 애그리게이트에 있는 객체를 변경하는 유일한 방법은 애그리게이트와 그 안의 객체 전체를 불러와서 애그리게이트 자체에 대해 메서드 호출하는 것 뿐
- 모델안에 컬렉션이 있으면 어떤 엔티티를 선정해서 그 엔티티와 관련된 모든 객체를 변경할 수 있는 단일 진입점으로 사용하게되면, 시스템이 개념적으로 더 간단해지고 추론하기가 쉬워짐
- 애그리게이트는 데이터 변경이라는 목적을 위해 한 단위로 취급할 수 있는 연관된 객체의 묶음
쇼핑몰 예시
장바구니라는 한 단위로 다뤄야하는 상품들로 이뤄진 컬렉션
여러 장바구니를 한 트랜잭션 안에서 바꾸고 싶지 않으므로 각 장바구니는 자신만의 불변조건을 유지할 책임을 담당하는 동시성경계
애그리게이트 선택
애그리게이트는 모든 연산이 일관성 있는 상태에서 끝난다는 점을 보장하는 경계
주문라인을 할당할 때는 주문라인으로 같은 SKU에 속한 배치에만 관심이 있음.
주어진 SKU에 속한 모든 배치의 컬렉션 > Product 라고 네이밍
class Product:
def __init__(self, sku: str, batches: List[Batch], version_number: int = 0):
self.sku = sku
self.batches = batches
self.version_number = version_number
def allocate(self, line: OrderLine) -> str:
try:
batch = next(b for b in sorted(self.batches) if b.can_allocate(line))
batch.allocate(line)
self.version_number += 1
return batch.reference
except StopIteration:
raise OutOfStock(f"Out of stock for sku {line.sku}")
- 도메인 모델에 있던 allocate함수를 product 클래스로 감싼다
- 이전에는 전체 배치목록을 얻어 모든 배치를 할당했다면 이제는 product 객체가 자신이 담당하는 SKU에 대한 모든 배치 담당
한 애그리게이트 = 한 저장소
애그리게이트가 도메인 모델에 접근 할 수 있는 유일한 통로여야 하기 때문에 모든 저장소는 애그리게이트만 반환해야함
BatchRepository 에서 PriductRepository로 저장소 변경
서비스 레이어도 맞춰서 변경해줌
def add_batch(
ref: str, sku: str, qty: int, eta: Optional[date],
uow: unit_of_work.AbstractUnitOfWork,
):
with uow:
product = uow.products.get(sku=sku)
if product is None:
product = model.Product(sku, batches=[])
uow.products.add(product)
product.batches.append(model.Batch(ref, sku, qty, eta))
uow.commit()
def allocate(
orderid: str, sku: str, qty: int,
uow: unit_of_work.AbstractUnitOfWork,
) -> str:
line = OrderLine(orderid, sku, qty)
with uow:
product = uow.products.get(sku=line.sku)
if product is None:
raise InvalidSku(f"Invalid sku {line.sku}")
batchref = product.allocate(line)
uow.commit()
return batchref
버전번호와 낙관적 동시성
- 특정 SKU에 해당하는 행들에만 락을 걸려면?
- Product 모델의 속성 하나를 사용해 전체 상태 변경이 완료됐는지 표시하고, 여러 동시성 작업자들이 이 속성 획득을 위해 경쟁하는 자원으로 활용
- 각 트랜잭션이 버전 넘버를 업데이트 하도록 한다면, 두 트랜잭션 경합시 한쪽만 버전이 올라간 Product를 커밋하게 되고 다른 트랜잭션은 실패하여 일관성이 유지됨
낙관적 동시성 제어 : 모든 일이 잘 돌아갈 것이라고 가정. 두 사용자의 데이터베이스 변경이 서로 충돌하는 경우가 드물다고 생각하기 때문에 일단 업데이트를 진행하고 문제 있을때 통지 받기
실패 처리시 일반적으로 실패한 연산을 처음부터 재시도 하는 방법사용
비관적 동시성 제어 : 두 사용자의 데이터베이스 변경이 충돌을 일으키기 쉽다고 가정해 안전성을 위해 모든 대상에 락을 사용. 성능을 고려해야함
버전번호 구현하는 방법
- 도메인에 있는 version_number 사용하는 방법 : 이를 product 추가하고 product.allocate()가 버전번호 증가 시킴
- 서비스 계층에서 commit()직접에 증가시킴 : 상태를 변경할 책임을 서비스와 도메인 계층에 나눠서 지저분해짐
- UoW와 저장소에서 처리 : 모든 제품이 변경됐다고 가정하지 않고 실제로 구현할 방법이 없어 이상적이지 않음
따라서 버전번호가 도메인 관심사와 무관하더라도 도메인에 추가하는 것이 가장 깔끔한 방법
마치며
- 동시성 제어는 비즈니스 환경이나 사용하려는 저장소 기술에 따라 매우 달라져서 책에서는 개념위주로 소개
- 애그리게이트는 모델의 일부 부분집합에 대한 주 진입점 역할 및 모든 모델 객체에 대한 비즈니스 규칙과 불변조건을 강제하는 역할 담당하도록 객체를 명시적으로 모델링
- 올바른 애그리게이트를 선택하는 것이 핵심이며 시간이 흐르면 애그리게이트로 선택한 객체가 달라질 수 도 있다
'책 & 스터디' 카테고리의 다른 글
[파이썬으로 살펴보는 아키텍처 패턴] 챕터 9 메시지 버스를 타고 시내로 나가기 (1) | 2024.05.15 |
---|---|
[파이썬으로 살펴보는 아키텍처 패턴] 챕터 8 이벤트와 메세지 버스 (1) | 2024.05.15 |
[파이썬으로 살펴보는 아키텍처 패턴] 챕터 6 작업단위 패턴 (0) | 2024.03.31 |
[파이썬으로 살펴보는 아키텍처 패턴] 챕터 5 높은 기어비와 낮은 기어비의 TDD (1) | 2024.03.31 |
[파이썬으로 살펴보는 아키텍처 패턴] 챕터 4 첫번째 유스 케이스: 플라스크 API와 서비스 계층 (1) | 2024.03.24 |