지금까진 도메인 규칙을 반영하는 소프트웨어에 초점을 맞췄다.
위의 도메인 규칙을 검증하는 유닛 테스트는 다음과 같다.
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
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
프로덕션 환경에서 경합조건으로 인한 문제를 해결하기 위해
작업의 일관성
이 보장돼야 했다.
UoW와 애그리게이트 패턴을 도입했고, 도메인 이벤트 패턴을 도입하여
비즈니스의 다양한 요구사항에 대해 빠르게 반영하는 구조로 변모했고 하나의 이벤트에 대해 수신하는 시스템이 각각 독립적으로 실패할 수 있도록 하여 시스템 안정성을 높였다.
도메인은 변함없다. 다만 읽기/쓰기는 접근 패턴이 다를 뿐이다.
예를 들어 고객이 매치의 상세 페이지를 접속하여 계속 화면을 보고 있는지, 다른 일을 하는지 알 수 없다.
하지만 고객이 주문을 신청할 때는 다르다.
이미 모집 완료된 매치에 추가 신청이 된다면 도메인 규칙을 기반으로 한
일관성 경계가 무너져 결국 시스템이 뒤엉킨다.
읽기 일관성, 그것은 가능한 일인가
'용산 아이파크몰 (2구장/맨유) 10시 매치'의 상세페이지에 접속했다.
자리가 하나 남았다. 부리나케 신청을 했지만, 내가 접속한 순간 다른 유저는 이미 주문을
진행하고 있었다. 결국 나의 신청은 실패한다.
다른 시나리오도 있다.
전주 지역의 매치를 지원한 유저가 있다. 하지만 그 매치가 진행되는 구장에 '골 때리는 그녀' 예능 프로그램의
긴급 촬영 일정이 잡혔다.
갑작스런 구장 일정 취소 통보에 당황하지만, 어쩔 수 없이 유저에게 환불을 진행해줘야 한다.
무엇을 하든, 현실은 소프트웨어 시스템과 일관성이 없다라는 사실을 받아들여야 할 때다.
읽기 | 쓰기 | |
---|---|---|
캐시 가능성 | 권장 | 불가능 |
일관성 | 불가능 | 필수 |
Post/리디렉션/Get 패턴
의 예시는 다음과 같다.
/batches 에 POST를 하여 새로운 배치를 만들면
유저를 /batches/123으로 리디렉션시켜 새로 만들어진 배치를 보여줄 수 있다.
위 기법은 명령-질의 분리(CQS, Command-Query Separation)의 예시다.
CQS는 하나의 간단한 규칙을 따른다.
함수는 상태를 변경하거나 질문에 답하는 일 중 단 한 가지만 해야 한다.
전등을 켜지 않고도 전등이 켜져 있는지에 대해 항상 대답할 수 있어야 하는 것이다.
allocations_view = Table(
'allocation_view', metadata,
Column('orderid', String(255)),
Column('sku', String(255)),
Column('batchref', String(255)),
)
정규화가 잘 된 스키마가 성능의 한계로 작용하는 상황이다.
따라서, 읽기 연산을 위해 최적화된, 데이터의 정규화되지 않은 복사본을 만들 수도 있다. 특히 인덱스를 사용하고도 성능의 한계를 만났다면 이런 복사본 대안을 택하게 된다.
잘 튜닝한 인덱스가 있어도, 관계형 데이터베이스는 조인을 위해 CPU를 아주 많이 사용한다.
가장 빠른 질의는 항상 SELECT * from mytable WHERE key = value;
다.
쿼리 성능 뿐만 아니라 스케일 아웃 관점에서도 장점이 있다.
최신 상태로 읽기 모델을 유지하는 방법을 DB 관점 이외에서 생각할 수는 없을까?
Allocated 이벤트에 대한 이벤트 핸들러를 정의하자.
EVENT_HANDLERS = {
events.Allocated: [
handlers.publish_allocated_event,
handlers.add_allocation_to_read_model,
]
}
업데이트 뷰 모델(update-view-model) 코드도 정의하자.
def add_allocation_to_read_model(
event: event.Allocated,
uow: unit_of_work.SqlAlchemyUnitOfWork,
):
with uow:
uow.session.execute(
'INSERT INTO allocations_view (orderid, sku, batchref)'
' VALUES(:orderid, :sku, :batchref)',
dict(orderid=event.orderid, sku=event.sku, batchref=event.batchref)
)
uow.commit()
이번엔 Deallocated 이벤트를 처리하는 코드다.
events.Deallocated: [
handlers.remove_allocation_from_read_model,
handlers.reallocate,
],
def remove_allocation_from_read_model(
event: events.Deallocated,
uow: unit_of_work.SqlAlchemyUnitOfWork,
):
with uow:
uow.session.execute(
'DELETE FROM allocations_view'
' WHERE orderid = :orderid AND sku = :sku'
읽기를 위한 모델링을 별도로 정의했다.
from django.db import models
from django.contrib.auth.models import User
class Manager(models.Model):
GRADE_CHOICES = (
(0, "교육생"),
(1, "실습생"),
(2, "BRONZE"),
(3, "SILVER"),
(4, "GOLD"),
(5, "PLATINUM"),
(6, "DIAMOND"),
)
user = models.OneToOneField(User, on_delete=models.CASCADE)
plab_zone = models.ForeignKey(
"stadium.PlabZone",
on_delete=models.CASCADE,
null=True,
blank=True,
)
is_open = models.BooleanField(default=True)
grade = models.SmallIntegerField(blank=True, null=True, choices=GRADE_CHOICES)
is_paid = models.BooleanField(default=False)
class Meta:
db_table = "manager"
MSA로 시스템 전환하는데만 시간을 쏟을 필요가 없다.
VOC를 먼저 마이크로 서비스로 시작함
무엇이든 점진적으로 전환하는게 가장 좋다