일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Java
- 스트림
- spring boot
- head first
- CQRS
- 디자인 패턴
- domain
- Stream
- Java8 in action
- Design Pattern
- spring Batch
- 자바8인액션
- 클린코드
- facade pattern
- Java8
- web
- 자바
- 패스트캠퍼스
- jsp
- Template Method Pattern
- 자바의 신
- SERVLET
- Java 8 in action
- 자바8
- Was
- ddd
- spring
- Java in action
- Clean Code
- AWS101
- Today
- Total
주난v 개발 성장기
[DDD START!] 10장. 이벤트 본문
구매를 취소하면 환불을 처리해야 한다.
이 때, 환불 기능을 실행하는 주체는 주문 도메인 엔티티가 될 수 있다.
도메인 객체에서 환불 기능을 실행하려면
1. 도메인 서비스를 파라미터로 전달받아 처리
public class Order {
public void cancel(RefundService refundService) {
// 처리
refundService.refund(getPaymentId());
}
}
- 주문 로직과 결제 로직이 섞이는 문제
- 기능 확장의 어려움 발생
- 메일을 보낸다고 했을 때, 메일 발송 Service를 파라미터에 추가해줘야한다.
- 더불어 트랜잭션 처리도 복잡해진다.
2. 응용 서비스에서 별도 환불 기능 실행
- CancelOrderService 구현
- 응용 서비스에서 CancelOrderService.cancel(); 호출
이 때 발생하는 궁금중...
-
환불 시스템의 응답이 없다면..? --> 답이 없음
-
트랜잭션 범위.(어디까지 롤백을 해야 할지)
이러한 문제가 발생하는 이유는 주문 BOUNDED CONTEXT와 결제 BOUNDED CONTEXT간의 강결합 때문이다.
-> 이를 해소하고자, 이벤트를 사용하고 특히 비동기 이벤트를 사용하면 결합도를 크게 낯출 수 있다.
이벤트란? 과거에 벌어진 어떤 것
이벤트 구성 요소
이벤트 주체 : (엔티티, 밸류, 도메인 서비스)와 같은 도메인 객체
- 도메인 로직을 실행해서 상태가 바뀌면 이벤트 발생
이벤트 핸들러 : 이벤트가 발생한 것에 대해 반응, 주문 취소 이벤트 발생 시 SMS 발송 가능
이벤트 디스패처(이벤트 퍼블릿셔) : 이벤트를 생성해서 디스패처에 전달하고, 핸들러로 전파한다.
이벤트의 구성
이벤트의 종류 : 클래스 이름으로 표현
이벤트 발생시간, 추가 데이터
Events.raise()는 디스패처를 통해 이벤트를 전파
public class Order {
public void changeShippingInfo(ShippingInfo shippingInfo) {
Events.rais(new ShippingInfoChangedEvent(number, shippingInfo);
}
}
public class ShippingInfoChangedHandler implement EventHandler<ShippingInfoChangedEvent> {
@Override
public void handle(ShippingInfoChangedEvent event) {
//
}
}
이벤트는 이벤트 핸들러가 작업을 수행하는 데 필요한 최소한의 데이터를 담아야 한다.
핸들러에서 DB를 읽어오거나 API를 호출해야하는 것은 이벤트를 사용하는 의미가 없어 보인다.
이벤트의 용도
1. 트리거 즉, 후처리 실행(환불 처리)
2. 이 기종간의 데이터 동기화 (외부 배송 서비스와 배송지 정보를 동기화)
이벤트의 장점
- 서로 다른 도메인 로직이 섞이는 것을 방지한다.
- 기능 확장도 용이
주문 취소 -> Event Dispatcher -> Event Handler -> 환불처리 + 이메일 통지
이벤트, 핸들러, 디스패처 구현
- Event 클래스
- EventHandler : 이벤트 핸들러를 위한 상위 타입, 모든 핸들러는 이 인터페이스를 구현한다.
- Events : 이벤트 디스패처, 이벤트 발행, 이벤트 핸들러 등록, 이벤트를 핸들러에 등록
Event Handler 인터페이스
EventHandler 인터페이슬 상속받는 클래스는 handle() 메서드를 이용해서 기능을 구현
canHandle() 메서드를 통해 이벤트 처리 가능 여부 확인
EventDispatcher인 Events 구현
도메인을 사용하는 응용 서비스는 이벤트를 받아 처리할 핸들러를 Events.handle()로 등록하고, 도메인 기능을 실행한다.
public class CancelOrderService {
private RefundService refundService;
@Transactional
public void cancel(OrderNo orderNo) {
Events.handle(
(OrderCanceledEvent event) -> refundService.refund(event.getOrderNumber())
);
...
}
}
Events는 내부적으로 핸들러 목록을 유지하기 위해 ThreadLocal을 사용
Events.handle()메서드는 인자로 받은 EventHandler를 List에 보관한다.
이벤트 발생 시 List를 찾아서 handle() 메서드를 호출해 이벤트를 처리한다.(Events.raise());
이벤트 처리 후에 Events.reset();을 통해 OOM 발생을 막는다.
--> AOP 처리도 가능하다.
도메인의 상태 변경과 이벤트 핸들러는 같은 트랜잭션 범위에서 실행된다.
동기 이벤트 처리 문제
비동기 이벤트 처리
-
로컬 핸들러를 비동기로 실행
- 이벤트 핸들러를 별도 스레드로 실행
- 287p 예제 코드를 보면,
동기 : handler.handle(event);
비동기 : executorService를 통해 새로운 스레드에서 실행
Events.handleAsync(); -
메시지 큐
이벤트가 발생하면, 이벤트 디스패처는 이벤트를 메시지 큐에 보낸다.
큐는 이벤트를 메시지 리스너에 전달하고, 메시지 리스너는 이벤트 핸들러를 이용해서 처리한다.
도메인 기능을 실행한 결과를 DB에 반영하고, 이벤트를 메시지 큐에 저장하는 것을 같은 트랜잭션 범위에서 해야한다면, 글로벌 트랜잭션 필요
--> 안전하지만, 성능이 떨어지는 단점이 있다.
많은 경우 이벤트를 발생하는 주체와 이벤트 핸들러가 별도 프로세스에서 동작한다.
(kafka는 글로벌 트랜잭션을 지원하지 않는다.) -
이벤트 저장소와 이벤트 포워더 사용
-
이벤트 저장소와 이벤트 제공 API 사용
이벤트 저장소를 이용한 비동기 처리
이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서, 이벤트 핸들러에게 전달하는 것
이벤트가 발생하면 핸들러는 스토리지에 이벤트를 저장.
포워더는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행한다.
포워더는 별도 스레드로 동작하기 때문에, 비동기로 처리가 된다.
또한, 동일 DB를 사용하므로 도메인의 상태변화와 이벤트 저장이 트랜잭션으로 처리된다.
API방식에서는 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져오는 방식.
포워더 방식 - 이벤트를 어디까지 처리했는지 추적하는 역할이 포워더에 있음
API 방식 - 이벤트 목록을 요구하는 외부 핸들러가 어디까지 처리했는지를 기억해야 한다.
포워더 방식, API 방식 모두 이벤트 저장소를 사용
EventStore : 이벤트를 저장하고 조회하는 인터페이스 제공
EventStore는 이벤트 객체를 직렬화 해서 payload에 저장한다.
Event는 과거에 벌어진 사건이므로, 데이터가 변경되지 않아 추가와 조회 기능만 제공된다.
API를 사용하는 클라이언트느 가장 마지막에 처리한 데이터의 오프셋인 lastOffset을 구해서, offset + 데이터 갯수를 lastOffset으로 저장한다.
lastOffset은 중복 처리 방지의 이유다.
포워더도 마찬가지로 마지막으로 전달한 이벤트의 오프셋을 기억했다가, 마지막 처리한 오프셋부터 이벤트를 가져온다.
Offset을 관리하는 OffsetStore를 구현한 클래스는 오프셋 값을 DB나 파일에 보관하면 된다.
이벤트 적용 시 추가 고려사항
- 이벤트 발생 주체 정보가 없어서, 특정 주체가 발생한 이벤트만 조회하는 기능을 구현할 수 없다.
- 포워더에서 전송 실패를 얼마나 허용할 것인지 재전송 횟수에 대한 제한 필요.
- 이벤트 손실 (로컬 핸들러를 이용해서, 비동기로 처리할 경우 실패하면 이벤트 유실 가능)
- 이벤트 순서
- 이벤트 재처리 - 순서, 재처리에 대한 케이스 (멱등성 유지)
'개발 성장기 > DDD' 카테고리의 다른 글
[DDD START!] 11장. CQRS (0) | 2020.06.12 |
---|---|
[DDD START!] 9장. 도메인 모델과 BOUNDED CONTEXT (0) | 2020.06.08 |
[DDD START!] 8장. 애그리거트 트랜잭션 관리 (1) | 2020.06.02 |
[DDD START!] 7장 도메인 서비스 (0) | 2020.05.29 |
[DDD START!] 6장. 응용 서비스와 표현 영역 (1) | 2020.05.26 |