Skip to content

Commit

Permalink
docs : DDD마무리
Browse files Browse the repository at this point in the history
  • Loading branch information
ywj9811 committed Jul 5, 2024
1 parent e3a53ba commit 8e46bf2
Show file tree
Hide file tree
Showing 31 changed files with 1,081 additions and 0 deletions.
274 changes: 274 additions & 0 deletions data/blog/DDDChap10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
---
title: 도메인 주도 개발 시작하기 Chap10
date: '2024-07-05'
tags: ['JAVA', '스터디', '기술서적', '도메인 주도 개발 시작하기']
draft: false
summary: 이벤트
---
# 이벤트

## 시스템 간 강결합 문제

예를 들어 쇼핑몰에서 구매를 취소하면 환불처리를 해야한다.

이때 여러 도메인이 함께 동작할 수 있는데, 주문 도메인에서 환불 기능을 제공하는 도메인 서비스를 받아서 처리하거나, 응용 서비스에서 처리할 수 있다.

하지만, 주로 결제 시스템은 외부에 위치하기 때문에 환불 도메인 서비스는 외부에 있는 결제 시스템이 제공하는 환불 서비스를 호출한다.

이때 두가지 문제가 발생할 수 있다.

1. 외부 서비스가 정상이 아닌 경우 트랜잭션 처리를 어떻게 해야할까?
2. 만약 환불을 처리하는 외부 시스템의 응답 시간이 길어지면, 서비스의 응답 시간도 같이 길어질 수 있다.

이러한 문제를 해결하는 방법으로는 이벤트 방식이 있다.

## 이벤트 개요

여기서 사용하는 이벤트는 ‘과거에 벌어진 어떤 것’ 을 의미한다.

### 이벤트 관련 구성요소

![Untitled](/static/images/DDD/101.png)

여기서, 도메인 모델을 대입하여 생각하면 다음과 같다.

- 이벤트 생성 주체 : 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체
- 이벤트 핸들러 : 이벤트 생성 주체가 발행한 이벤트에 반응하는 부분
- 이벤트 디스패처 : 이벤트 생성 주체와 이벤트 핸들러를 연겨해 주는 것

이때 이벤트 디스패처의 구현 방식에 따라서 이벤트 생성과 처리를 동기로 하거나 비동기로 하거나 나뉜다.

### 이벤트 구성

이벤트는 발생한 정보를 담는다.

이벤트 핸들러는 디스패처로 부터 이벤트를 전달받아 이벤트 처리에 필요한 작업을 수행한다.

### 이벤트 용도

이벤트는 크게 두가지 용도로 쓰인다.

1. 트리거

도메인의 상태가 바뀔 때 다른 후처리가 필요하면 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.

주문에서는 주문 취소 이벤트를 트리거로 사용할 수 있는 것이다.

![Untitled](/static/images/DDD/102.png)


1. 서로 다른 시스템 간의 데이터 동기화

배송지를 변경하면 외부 배송 서비스에 바뀐 배송지 정보를 전송해야 하는데, 주문 도메인은 배송지 변경 이벤트를 발생시키고, 이벤트 핸들러는 외부 배송 서비스와 배송지 정보를 동기화 할 수 있다.


### 이벤트 장점

이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.

![Untitled](/static/images/DDD/103.png)

위의 경우는 예시로 들은 주문 도메인에서 환불 기능을 제공하는 도메인 서비스를 받아서 처리하는 경우를 이벤트를 사용하여 분리한 것이다.

이벤트를 사용하여 주문 도메인에서 환불을 위한 결제 도메인에 대한 의존을 제거했다.

그리고, 이벤트를 사용하면 확장에도 용이하다.

만약 이메일을 보내는 로직이 추가로 필요하다면, 도메인에서는 해당 이벤트를 추가로 발생시키기만 하면 된다.

## 이벤트, 핸들러, 디스패처 구현

- 이벤트 클래스 : 이벤트를 표현
- 이벤트 디스패처 : 스프링이 제공하는 ApplicationEventPublisher를 이용
- Events : 이벤트를 발행한다. 이벤트 발행을 위해 ApplicationEventPublisher를 이용
- 이벤트 핸들러 : 이벤트를 수신해서 처리한다. 스프링이 제공하는 기능 사용

### 이벤트 클래스

정해진 타입은 없다. 원하는 클래스를 이벤트로 사용하면 된다.

주로, 과거 시제를 사용하여 클래스명을 만든다.

예를 들어 `OrderCanceledEvent` 혹은 `OrderCanceled` 이렇게 만든다.

```java
public OrderCanceledEvent {
private String orderNumber;

// getter, constructure ...
```

모든 이벤트가 공통으로 갖는 프로퍼티가 존재한다면 관련 상위 추상 클래스를 만들고 사용하기 또한 가능하다.

### Events 클래스와 ApplicationEventPublisher

이벤트 발생과 출판을 위해 스프링이 제공하는 ApplicationEventPublisher를 이용한다.

Events 클래스는 ApplicationEventPublisher를 사용해서 이벤트를 발생시키도록 구현할 것이다.

```java
public class Events {
private static ApplicationEventPublisher publisher;

//setter

public static void raise(Object event) {
if (publisher != null) {
publisher.publishEvent(event);
}
}
}
```

Events 클래스의 `raise()` 는 ApplicationEventPublisher이 제공하는 `publishEvent()` 를 이용해서 이벤트를 발생시킨다.

```java
@Configuration
public class EventConfiguration {
@Autowired
private ApplicationContext applicationContext;

@Bean
public InitializingBean eventsInitializer() {
return () -> Events.setPublisher(applicationContext);
}
}
```

이렇게 Events 클래스를 초기화 하고 사용할 수 있다.

### 이벤트 발생과 이벤트 핸들러

`Events.raise()` 를 통해 이벤트를 발생시키는데, 이제 이벤트를 처리해야 한다.

이벤트를 처리할 핸들러는 스프링이 제공하는 `@EventListner` 어노테이션을 사용해서 구현한다.

```java
@EventListener(OrderCanceledEvent.class)
public void handle(OrderCanceledEvent event) {
refundService.refund(event.getOrderNumber());
}
```

이런식으로 만들 수 있다.

이때 이벤트를 발생시킬 때 사용한 타입 객체를 가지는 `@EventListener` 가 붙은 메소드를 찾아서 실행시킨다.

### 흐름 정리

1. 도메인 기능 실행
2. 도메인 기능은 `Events.raise()` 를 사용하여 이벤트 발생
3. `Events.raise()` 는 스프링이 제공하는 기능을 사용하여 이벤트를 출판
4. `@EventListener` 를 찾아서 실행

이 흐름에 따르면 응용 서비스와 동일한 트랜잭션 범위에서 이벤트 핸들러를 실행하고 있다.

즉, 도메인 상태 변경과 이벤트 핸들러는 같은 트랜잭션 범위에서 실행된다.

## 동기 이벤트 처리 문제

지금까지 도메인간의 결합을 해결했다.

하지만 외부 시스템에 대한 영향은 해결하지 못했다.

만약 외부 환불 서비스가 실행에 실패했다고 하더라도, 반드시 트랜잭션을 롤백할 필요가 없다면?

구매 취소는 성공 시키고, 이후에 환불을 따로 재처리 할 수 있을 것이다.

이를 위해서는 외부 시스템과 연동을 동기로 처리하지 않고 비동기로 처리하거나 이벤트와 트랜잭션을 연계하는 방식이 있다.

### 비동기 이벤트 처리

4가지 방식으로 비동기 이벤트 처리를 구현하는 것을 알아보자.

1. 로컬 핸들러를 비동기로 실행하기
2. 메시지 큐 사용하기
3. 이벤트 저장소와 이벤트 포워더 사용하기
4. 이벤트 저장소와 이벤트 제공 API 사용하기

### 로컬 핸들러 비동기 실행

이때는 단순하게 이벤트 핸들러를 별도의 스레드로 실행하는 것이다.

스프링이 제공하는 `Async` 어노테이션을 사용하면 손쉽게 비동기로 구현할 수 있다.

이를 위해 두가지 준비가 필요하다.

1. `@EventAsync` 어노테이션을 사용해 비동기 기능 활성화
2. 이벤트 핸들러 메소드에 `@Async` 어노테이션을 붙인다.

이렇게 하면 기존의 코드를 크게 수정하지 않고 비동기로 변경할 수 있다.

### 메시징 시스템을 이용한 비동기 구현

기존의 방식이 아닌, 메시지 큐를 이용하는 방식이다.

Kafka 혹은 RabbitMQ 와 같은 메시징 시스템을 사용하는 것이다.

이러한 방식을 사용하면 이벤트를 메시지 큐에 저장하는 과정과 메시지 큐에서 이벤트를 읽어와 처리하는 과정은 별도의 스레드나 프로세스로 처리한다.

하지만, 이때 모든 과정을 하나의 트랜잭션 범위에서 실행하기 위해서는 글로벌 트랜잭션이 필요하며 이는 성능이 안좋아질 수 있다.

참고로, RabbitMQ는 글로벌 트랜잭션을 지원하지만, Kafka는 글로벌 트랜잭션을 지원하지 않는다 한다.

### 이벤트 저장소를 이용한 비동기 처리

이벤트를 비동기로 처리하는 또 다른 방법은, 이벤트를 DB에 저장하고, 주기적으로 읽어와 이벤트 핸들러에 전달하는 것이다.

이때 두가지 방식이 있으며, 이를 그림으로 확인하자.

![Untitled](/static/images/DDD/104.png)

![Untitled](/static/images/DDD/105.png)

## 이벤트 적용시 추가 고려사항

다만, 이벤트를 사용하면 추가로 고려할 사항이 있다.

- 이벤트 저장소에 이벤트의 발생 주체에 대한 정보를 추가할지 고려하자.
- 이벤트 저장소의 포워더에서 전송 실패를 얼마나 허용하고, 어떻게 처리할지 고려하자.
- 이벤트 저장소를 하나의 트랜잭션에서 수행하면 보관이 보장되지만, 로컬 핸들러를 사용하는 경우 이벤트가 유실되는 위험이 있다는 것을 고려하자.
- 이벤트 순서를 보장하는 방법에 대해 고려하자.
- 이벤트 재처리를 어떻게 수행할지 고려하자.

## 이벤트 처리와 DB 트랜잭션 고려

DB 트랜잭션 관점에서 고려할 점이 있다.

1. 이벤트 발생과 처리를 모두 동기로 처리한다면

![Untitled](/static/images/DDD/106.png)

만약 위에서 13번 과정이 실패하면 모두 롤백을 하는것이 맞을까 생각을 해볼 필요가 있을 것이다.

2. 이벤트를 비동기로 처리한다면

![Untitled](/static/images/DDD/107.png)


10번까지 성공했지만, 12번이 실패한다면 결제는 취소되지 않을 것이다.

이에 대한 처리를 고민할 필요가 있을 것이다.


이벤트 처리에 대해 동기/비동기 상관 없이 트랜잭션 실패를 함께 고려해야 할 것이다.

하지만 이를 모두 고려하기 위해서는 너무 많은 경우의 수가 있다.

이러한 경우의 수를 줄이는 방법중에 하나는 트랜잭션이 성공할 때만 이벤트 핸들러를 처리하는 방식이 있다.

이를 위해서 스프링은 `@TransactionalEventListener` 어노테이션을 지원하는데, 이는 스프링 트랜잭션 상태에 따라 이벤트 핸들러를 실행할 수 있게 해준다.

```java
@TransactionalEventListener(
classes = OrderCanceledEvent.class
phase = TransactionPhase.AFTER_COMMIT
)
public void handle(OrderCanceledEvent event) {
...
}
```

위의 코드에서 phase에 대한 설정을 AFTER_COMMIT 으로 하였는데, 이를 통해 트랜잭션 커밋에 성공한 뒤 핸들러 메소드를 실행한다.

만약 중간에 에러가 발생해 트랜잭션이 롤백되면 핸들러 메소드를 실행하지 않는다.
54 changes: 54 additions & 0 deletions data/blog/DDDChap11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: 도메인 주도 개발 시작하기 Chap11
date: '2024-07-05'
tags: ['JAVA', '스터디', '기술서적', '도메인 주도 개발 시작하기']
draft: false
summary: CQRS
---
# CQRS

## 단일 모델의 단점

예를 들어 쇼핑몰에서 주문 내역 조회 기능을 구현하면 여러 애그리거트에서 데이터를 가져와야 한다.

Order에서 주문 정보, Product에서 상품 정보, Member에서 회원 정보를 가져와야 한다.

조회 화면은 조회 속도가 빠를수록 좋은데 여러 애그리거트의 데이터가 필요하면, 구현 방법을 고민해야 한다.

이를 위해서 여러가지 방법을 시도해도 계속해서 고려할 부분이 많을 것이다.

이런 고민이 생기는 이유는 시스템 상태를 변경할 때와 조회할 때 단일 도메인 모델을 사용하기 때문이다.

도메인 모델을 구현할 때 사용하는 ORM 기법은 도메인 상태 변경 기능을 구현하는데는 적합하지만, 여러 애그리거트에서 데이터를 동시에 가져올 때는 구현을 복잡하게 만드는 원인이 된다.

이때 상태 변경을 위한 모델과 조회를 위한 모델을 분리하면 좋은 방식이 된다.

## CQRS

시스템이 제공하는 기능은 크게 두가지로 나눌 수 있는데, 하나는 상태를 변경하는 기능이고 하나는 조회하는 기능이다.

CQRS는 Command Query Responsibility Segregation 의 약자로, 상태를 변경하는 명령 모델과 상태를 제공하는 조회 모델을 분리하는 패턴이다.

![Untitled](/static/images/DDD/111.png)

이러한 CQRS를 사용하면 각 모델에 맞는 구현 기술을 선택할 수 있다.

예를 들어 명령 모델은 객체 지향에 기반해 도메인 모델을 구현하기에 적당한 JPA를 사용하고 조회 모델은 DB 테이블에서 SQL로 데이터를 조회할 때 좋은 MyBatis를 사용할 수 있다.

혹은 명령 모델과 조회 모델은 서로 다른 데이터 저장소를 사용할 수 있다.

명령 모델은 트랜잭션을 지원하는 RDBMS를 사용하고 조회 모델은 조회 성능이 좋은 NoSql을 사용하는 것이다.

![Untitled](/static/images/DDD/112.png)

다만 이때 두 저장소 간에 동기화를 시켜야 하는데 이를 10장에서 살펴본 이벤트를 사용할 수 있다.

## CQRS 장단점

CQRS 패턴을 적용할 때 얻을 수 있는 장점은 명령 모델을 구현할 때 도메인 자체에 집중할 수 있다는 점이다.

복잡한 도메인은 주로 상태 변경 로직이 복잡한데 명령 모델과 조회 모델을 구분하면 조회 성능을 위한 코드가 명령 모델에 없으므로 도메인 로직을 구현하는데 집중할 수 있다.

반면, 트래픽이 적거나 도메인이 단순한 경우 조회 전용 모델을 따로 만들 때 얻을 수 있는 이점에 대해 따져보아야 한다.

더 많은 기술이 필요하고, 복잡성이 늘어나기 때문에 여러가지를 고려하여 도입하면 좋다.
Loading

0 comments on commit 8e46bf2

Please sign in to comment.