티스토리 뷰
목차
1. 도메인 이벤트
2. 스프링 ApplicationEvent
3. 이벤트 소싱(Event Sourcing)
4. CQRS(명령 및 쿼리 책임 분리)
1. 도메인 이벤트
느슨한 결합과 강한 결합
- 외부 서비스가 정상이 아닐 경우 트랜잭션 처리를 어떻게 해야 할지 애매
- 외부 서비스 성능에 직접적인 영향을 받는 문제가 있다.
- 도메인 객체에 서비스를 전달하면 추가로 설계상 문제가 나타날 수 있다.
- 도메인 객체에 서비스를 전달할 떄 또 다른 문제는 기능을 추가할 때 발생한다.
- 비동기 이벤트를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다.
이벤트
- 이벤트가 발생한다는 것은 상태가 변경됐다는 것을 의미한다.
- 도메인 모델에서도 UI 컴포넌트와 유사하게 도메인의 상태 변경을 이벤트로 표현할 수 있다.
- 보통 '~할 때', '~가 발생하면', '만약 ~하면'과 같은 요구사항은 도메인의 상태 변경과 관련된 경우가 많고 이런 요구사항을 이벤트를 이용해서 구현할 수 있다.
- 이벤트는 이미 과거에 일어난 사건이기 때문에 수정되거나 삭제되지 않는다.
이벤트 관련 구성요소
- 도메인 모델에서 이벤트 주체는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체이다.
- 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생한다.
- 이벤트 핸들러(handler)는 이벤트 생성 주체가 발생한 이벤트에 반응한다.
- 이벤트 핸들러는 생성 주체가 발생한 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다.
- 이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 이벤트 디스패처(dispatcher)이다.
- 이벤트를 전달받은 디스패처는 해당 이벤트를 처리할 수 있는 핸들러에 이벤트를 전파한다.
이벤트의 구성
- 이벤트는 현재 기준으로 (바로 직전이라도) 과거에 벌어진 것을 표현하기 때문에 이벤트 이름에는 과거 시제를 사용한다.
- 이벤트는 이벤트 핸들러가 작업을 수행하는 데 필요한 최소한의 데이터를 담아야 한다.
이벤트 용도
- 도메인의 상태가 바뀔 때 다른 후처리를 해야 할 경우 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.
- 이벤트의 두 번째 용도는 서로 다른 시스템 간의 데이터 동기화이다.
이벤트 장점
- 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.
- 이벤트 핸들러를 사용하면 기능 확장도 용이하다.
비동기 이벤트 처리
- 로컬 핸들러를 비동기로 실행하기
- 메시지 큐를 사용하기
- 이벤트 저장소와 이벤트 포워더 사용하기
- 이벤트 저장소와 이벤트 제공 API 사용하기
2. 스프링 ApplicationEvent
1) Event Publisher 사용
[ Event Publisher ]
@Transactional
@Service
public class ProductService {
private final ApplicationEventPublisher publisher;
public ProductService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void create(String name, int price) {
Product product = new Product(name, BigDecimal.valueOf(price));
publisher.publishEvent(new ProductCreatedEvent(name));
}
}
public class ProductCreatedEvent {
private UUID id;
private final String name;
private final int price;
public ProductCreatedEvent(UUID id, String name, int price) {
this.id = id;
this.name = name;
this.price = price;
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
[ Event Listener ]
@Component
public class EmailSender implements Sender {
@EventListener
public void send(ProductCreatedEvent event) {
System.out.println("email sender = " + event.getName());
}
@Override
public void send(String text) {
System.out.println("email sender = " + text);
}
}
@Component
public class SmsSender implements Sender {
@EventListener
public void send(ProductCreatedEvent event) {
System.out.println("sms sender = " + event.getName());
}
@Override
public void send(String text) {
System.out.println("sms sender = " + text);
}
}
@EventListener는 이벤트를 발행하는 트랜잭션에 묶여있기 때문에 send() 에서 실행이 실패하면 이벤트를 발행하는 로직이 롤백된다.
이 문제는 @TransactionalEventListener를 사용해 트랜잭션 범위 밖에서 실행되도록 할 수 있다.
@TransactionalEventListener
public void send(ProductCreatedEvent event) {
System.out.println("email sender = " + event.getName());
}
특정 조건에 의해 이벤트 로직이 수행되어야 할 때 이벤트 발행은 조건을 체크하는 로직이 들어가지 않도록 작성하고
이벤트를 수신하는 쪽에서 조건에 따라 로직이 처리되도록 작성할 수 있다.
@TransactionalEventListener(condition = "#event.price != 0")
public void send(ProductCreatedEvent event) {
System.out.println("email sender = " + event.getName());
}
[ Test ]
@RecordApplicationEvents 를 사용해 이벤트 목록 주입받을 수 있다.
다음과 같은 테스트는 비용이 크기 때문에 단위 테스트 보다는 인수 테스트에서 작성해볼 수 있다.
@RecordApplicationEvents
@SpringBootTest
class ProductServiceTest {
@Autowired
ProductService productService;
@Autowired
private ApplicationEvents events;
@Test
void create() {
assertThat(events.stream(ProductCreatedEvent.class).count()).isEqualTo(0);
productService.create("치킨", 10_000);
assertThat(events.stream(ProductCreatedEvent.class).count()).isEqualTo(1);
}
}
2) Abstract Aggregate Root
명시적으로 AbstractAggregateRoot 클래스를 상속하도록 작성한다.
엔티티에서 이벤트를 register 하고 응용 서비스에서 명시적으로 Spring Data Jpa 의 save()를 호출해 이벤트를 발행한다.
@Transactional
@Service
public class ProductService {
private final ProductRepository productRepository;
private final ApplicationEventPublisher publisher;
public void changePrice(UUID id, int price) {
Product product = productRepository.findById(id).orElseThrow(EntityNotFoundException::new);
product.changePrice(BigDecimal.valueOf(price));
// 이벤트 발행을 위해 save()를 명시적으로 호출해줘야 한다.
productRepository.save(product);
}
}
@Entity
public class Product extends AbstractAggregateRoot<Product> {
@Column(name = "id", columnDefinition = "varbinary(16)")
@Id
private UUID id;
// ...
public void changePrice(BigDecimal price) {
this.price = new ProductPrice(price);
registerEvent(new ProductPriceChangedEvent(id, price));
}
}
public class ProductPriceChangedEvent {
private final UUID id;
private final BigDecimal price;
public ProductPriceChangedEvent(UUID id, BigDecimal price) {
this.id = id;
this.price = price;
}
}
엔티티의 ID를 @GeneratedValue를 이용해 자동 생성하는 경우 엔티티를 DB에 저장하기 전에는 ID를 알 수 없는 문제가 발생한다.
이런 경우 @PostPersist를 사용해 엔티티가 영속화된 경우 이벤트를 생성하도록 할 수 있다.
// service
public Product create(String name, int price) {
Product product = productRepository.save(new Product(name, BigDecimal.valueOf(price)));
return product;
}
// entity
@PostPersist
private void created() {
registerEvent(new ProductCreatedEvent(id, name, price));
}
3. 이벤트 소싱(Event Sourcing)
개념
- 도메인 모델에서 발생하는 모든 이벤트를 기록하는 데이터 저장 기법
- 반응형 시스템에 적합하고 규모 확장 용이
- 로깅은 예외 상황이나 프로파일링까지 고려하지만 Event Sourcing은 비즈니스 이벤트에 대해서만 다룬다.
- Update나 Delete 연산은 수행되지 않는다.
입출금 예시
이벤트: 사용자 A가 통장 a에 1000원을 입금(+)한다.
현재 상태: 1000원
이벤트: 사용자 A가 통장 a에 500원을 인출(-)한다.
현재 상태: 500원
이벤트: 사용자 A가 통장 a에 1000원을 입금(+)한다.
현재 상태: 1500원
4. CQRS(명령 및 쿼리 책임 분리)
CQRS
- 상태를 변경하는 명령(Command)을 위한 모델과 상태를 제공하는 조회(Query)를 위한 모델을 분리하는 패턴이다.
- CQRS는 복잡한 도메인에 적합하다.
웹과 CQRS
- 메모리에 캐시하는 데이터는 DB에 보관된 데이터를 그대로 저장하기보다는 화면에 맞는 모양으로 변환한 데이터를 캐시할 때 성능에 더 유리하다.
- 조회 속도를 높이기 위해 별도 처리를 하고 있다면 명시적으로 명령 모델과 조회 모델을 구분하자.
- 조회 기능 때문에 명령 모델이 복잡해지는 것을 방지할 수 있고 명령 모델에 관계없이 조회 기능에 특화된 구현 기법을 보다 쉽게 적용할 수 있다.
CQRS 장단점
- 명령 모델을 구현할 때 도메인 자체에 집중할 수 있다.
- 조회 성능을 향상시키는 데 유리하다.
- 구현해야 할 코드가 더 많다.
- 더 많은 구현 기술이 필요하다.
출처
NEXTSTEP: DDD 세레나데 3기
'DDD' 카테고리의 다른 글
[DDD] 4. 도메인 주도 설계 아키텍처 (0) | 2022.05.31 |
---|---|
[DDD] 3. 도메인 주도 설계 기본 요소 (0) | 2022.05.30 |
[DDD] 2. 도메인 모델링 (0) | 2022.05.28 |
[DDD] 1. 도메인 주도 설계 이해 (0) | 2022.05.27 |
- Total
- Today
- Yesterday
- named query
- mockito
- 트랜잭셔널 아웃박스 패턴
- 이벤트 스토밍
- Stream
- Ubiquitous Language
- http
- 폴링 발행기 패턴
- Spring Boot
- 도메인 모델링
- 스프링 카프카 컨슈머
- 육각형 아키텍처
- JPA
- 클린코드
- Spring
- java8
- Git
- Spring Data JPA
- clean code
- MySQL
- 스프링 예외 추상화
- 학습 테스트
- kafka
- ATDD
- 마이크로서비스 패턴
- TDD
- spring rest docs
- HTTP 헤더
- 계층형 아키텍처
- H2
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |