파이썬으로 살펴보는 아키텍처 패턴 - 10장 (커맨드와 커맨드 핸들러)

이동욱

2022/01/05

커맨드와 이벤트


이벤트와 커맨드의 비교

class Command:
    pass

@dataclass
class Allocate(Command):
    orderid: str
    sku: str
    qty: int

@dataclass
class CreateBatch(Command):
    ref: str
    sku: str
    qty: int
    eta: Optional[date] = None

@dataclass
class ChangeBatchQuantity(Command):
    ref: str
    qty: int

예외 방식의 차이점


def handle(
        message: Message,
        uow: unit_of_work.AbstractUnitOfWork,
):
    results = []
    queue = [message]
    while queue:
        message = queue.pop(0)
        if isinstance(message, events.Event):
            handle_event(message, queue, uow)
        elif isinstance(message, commands.Command):
            cmd_result = handle_command(message, queue, uow)
            results.append(cmd_result)
        else:
            raise Exception(f"{message} was not an Event or Command")
    return results


def handle_event(
        event: events.Event,
        queue: List[Message],
        uow: unit_of_work.AbstractUnitOfWork
):
    for handler in EVENT_HANDLERS[type(event)]:  # 한 이벤트를 여러 핸들러가 처리하도록 위임할 수 있는 디스패처로 이벤트가 처리된다.
        try:
            logger.debug('handling event %s with handler %s', event, handler)
            handler(event, uow=uow)
            queue.extend(uow.collect_new_events())
        except Exception:
            logger.exception('Exception handling event %s',
                             event)  # 오류가 발생하면 오류를 찾아서 로그에 남기지만, 오류가 메시지 처리를 방해하지는 못하게 한다.
            continue


def handle_command(
        command: commands.Command,
        queue: List[Message],
        uow: unit_of_work.AbstractUnitOfWork
):
    logger.debug('handling command %s', command)
    try:
        handler = COMMAND_HANDLERS[type(command)]  # 커맨드 디스패처는 커맨드 한 개에 핸들러 한 개만 허용한다.
        result = handler(command, uow=uow)
        queue.extend(uow.collect_new_events())
        return result
    except Exception:
        logger.exception('Exception handling command %s', command)
        raise  # 오류가 발생하면 빠르게 실패하면서 오류를 위로 전파한다.


EVENT_HANDLERS = {
    events.OutOfStock: [handlers.send_out_of_stock_notification],
}  # type: Dict[Type[events.Event], List[Callable]]

COMMAND_HANDLERS = {
    commands.Allocate: handlers.allocate,
    commands.CreateBatch: handlers.add_batch,
    commands.ChnageBatchQuantity: handlers.change_batch_quantity,
}  # type: Dict[Type[commands.Command], Coallable]

논의: 이벤트, 커맨드, 오류 처리


주문 이력이 2개 있는 고객이 3번째 주문을 넣을 때 이 고객을 VIP로 설정한다. 처음 VIP 로 변경된 고객에게는 축하 이메일을 보낸다.

class History:
    def __init__(self, customer_id: int):
        self.orders = set()
        self.customer_id = customer_id

    def record_order(self, order_id, str, order_amount: int):
        entry = HistoryEntry(order_id, order_amount)

        if entry in self.sorders:
            return

        self.orders.add(entry)

        if len(self.orders) == 3:
            self.events.append(
                CustomerBecameVIP(self.customer_id)
            )


def create_order_from_basket(uow, cmd: CreateOrder):
    with uow:
        order = Order.from_basket(cmd.customer_id, cmd.basket_items)
        uow.orders.add(order)
        uow.commit()


def update_customer_history(uow, event: OrderCreated):
    with uow:
        history = uow.order_history.get(event.customer_id)
        history.record_order(event.order_id, event.order_amount)
        uow.commit()


def congratulate_vip_customer(uow, event: CustomerBecameVIP):
    with uow:
        customer = uow.customer.get(event.customer_id)
        email.send(
            customer.email_address,
            f'Congratulations {customer.first_name}'
        )

동기적으로 오류 복구 하기


def handle_event(
        event: events.Event,
        queue: List[Message],
        uow: unit_of_work.AbstractUnitOfWork
):
    for handler in EVENT_HANDLERS[type(event)]:  # 한 이벤트를 여러 핸들러가 처리하도록 위임할 수 있는 디스패처로 이벤트가 처리된다.
        try:
            for attempt in Retrying(
                stop=stop_after_attemp(3),
                wait=wait_exponential()
            ):
                with attempt:
                    logger.debug("handling event %s with hanler %s", event, handler)
                    handler(event, uow=uow)
                    queue.extend(uow.collect_new_events())
        except Exception:
            logger.error(
                'Failed to handle event %s times, giving up', retry_failure.last_attempt.attempt_number
            )
            continue

커맨드와 이벤트 분리의 트레이드 오프

참고 문헌


>> Home