OrderCommandHandlers
: 메시지 채널에서 들어온 커맨드 메시지를 받아 비즈니스 로직을 호출하는 인바운드 어댑터OrderServce
, OrderDao
Order
Order
클래스의 상태 값은 프라이빗하기에 메서드를 통한 간접 접근만 가능Money
클래스)Order
비즈니스 객체에 어떤 작업을 수행한다고 하자.
Order
객체를 조회하거나 변경을 일으킬 것이다.Order
뿐 아니라 주문 품목, 지불 정보 등 다른 연관 데이터도 많다.Order
객체는 ’최소 주문량’이라는 불변 값이 있어서 주문을 업데이트할 때 이 값 이상을 주문해야 한다.아래의 트랜잭션 들은 별도의 UI 시나리오에서 진행
소비자1 소비자 2
BEGIN TXN BEGIN TXN
주문 ID로 주문 조회 주문 ID로 주문 조회
주문 ID로 주문 품목 조회 주문 ID로 주문 품목 조회
... ...
END TXN END TXN
최소 주문량이 충족됨을 확인
BEGIN TXN
주문 품목 수량을 낙관적 락을 통해 업데이트
END TXN 최소 주문량이 충족됨을 확인함
BEGIN TXN
주문 품목 수량을 낙관적 락을 통해 업데이트
END TXN
Order
는 최소 주문량을 못맞추게 되어 더 이상 유효하지 않게 됨Order
애그리거트와 그 경계
Order
는 애그리거트 루트이며 DeliveryInfo
, PaymentInfo
, OrderLineItem
은 벨류 객체OrderLineItem
)을 직접 건드리면 위험한 이유는 위 예제에서 설명했다.Order
는 다른 루트 엔티티인 Cunsumer
객체 대신 consumerId
로 참조해야 한다.Consumer
애그리거트 루트를 통해 소비자-주문을 원자적으로 업데이트하거나 여러 주문을 원자적으로 업데이트할 수도 있다.OrderCreated
애그리거트 ID가 이벤트의 프로퍼티가 아닌 엔벨로프의 일부일 수 있다.
// 자신을 구현한 클래스가 도메인 이벤트임을 알리는 마커 인터페이스
interface DomainEvent {}
// 주문 애그리거트 이벤트임을 알리는 마커 인터페이스
interface OrderDomainEvent extends DomainEvent{}
// 주문 생성 도메인 이벤트
class OrderCreatedEvent implements OrderDomainEvent {}
// 이벤트 객체 및 메타데이터를 조회하는 메서드가 존재
interface DomainEventEvelop<T extends DomainEvent> {
String getAggregateId();
Message getMessage();
String getAggregateType();
String getEventId();
T getEvent();
}
도메인에서 이벤트를 반환하고 서비스에서 발행하는 방법
@RequiredArgsConstructor
public class KitchenService {
private final TicketRepository ticketRepository;
private final TicketDomainEventPublisher domainEventPublisher;
public void accept(long ticketId, LocalDateTime readyBy) {
Ticket ticket = ticketRepository.findById(ticketId).orElseThrow(...);
List<TicketDomainEvent> events = ticket.accept(readyBy);
domainEventPublisher.publish(Ticket.class, orderId, events);
}
}
Ticket.accept()
메서드가 이벤트를 반환하고 서비스에서 발행애그리거트 루트의 필드에 이벤트를 쌓고 서비스가 가져다 발행하는 방법
public class Ticket extends AbstractAggregateRoot {
public void accept(LocalDateTime readyBy) {
...
registerEvent(new TicketAcceptedEvent(readyBy));
}
}
registerEvent()
는 AbstractAggregateRoot
에 정의된 메서드AbstractAggregateRoot.domainEvents()
호출해서 이벤트를 가져와 발행할 수 있다.AbstractAggregateDomainEventPublisher
타입 안전한 도메인 이벤트 발행용 인터페이스를 제공하는 추상/제네릭 클래스
public abstract class AbstractAggregateDomainEventPublisher<A, E extends DomainEvent> {
...
}
A
)과 이벤트 마커 인터페이스 타입(E
)를 정의KitchenService
가 Ticket
애그리거트의 이벤트 마커 인터페이스인 TicketDomainEvent
만 발행해야 한다는 제약을 구현할 수 있다.DomainDispatcher
같은 고수준 API를 사용할 수 있다.KitchenServiceEventConsumer
주방 서비스의 데이터 레플리카를 항상 최신 상태로 유지
@RequiredArgsConstructor
public class KitchenServiceEventConsumer {
private final private KitchenService kitchenService;
public DomainEventHandlers domainEventHandlers() { // 이벤트와 이벤트 핸들러를 매핑
return DomainEventHandlersBuilder
.forAggregateType("net.chrisrichardson.ftgo.restaurantservice.Restaruant")
.onEvent(RestaruantMenuRevised.class, this::reviseMenu)
.build();
}
public void reviseMenu(DomainEventEnvelop<RestaruantMenuRevised> de) { // 로직 처리
long id = Long.parseLong(de.getAggregateId());
restaurantMenu revisedMenu = de.getEvent().getRevisedMenu();
kitchenService.reviseMenu(id, reviseMenu);
}
}