이벤트

2023. 9. 9. 21:32스터디/DDD 스터디

728x90

## 시스템 간 강결합 문제

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

보통 결제 시스템은 외부에 있으므로 Order 도메인에서 구매 취소에 관련된 서비스를 다음과 같이 파라미터로 주입할 것이다

위처럼 외부 시스템을 도메인에서 호출 시 3가지 문제가 발생할 수 있다.

 

1. 외부 서비스가 비정상일 경우 트랜젝션 처리를 어떻게 할까?

- 롤백을 해야할까? 일단 커밋을 해야할까?, 아니면 상태만 변경한 후에 나중에 다시 시도를 해야할까?

2. 외부 시스템의 응답 시간이 길어지면 어떻게 할까?

- 대기 시간만큼 응답시간이 길어져서 성능에 악영향을 주지 않을까?

3. 도메인 객체에 서비스를 전달하면 설계상 문제가 발생하지 않을까?

- 도메인 로직과 외부 로직이 뒤섞이지 않을까?

 

위 문제가 발생하는 이유는 바운디드 컨텍스트 간 강결합 때문이다.

비동기 이벤트를 사용하면 시스템 간 결합을 크게 낮출 수 있다.

 

## 이벤트 개요

- 이벤트 : 과거에 벌어진 어떤 것, 이벤트가 발생했다는 것은 상태가 변경됐다는 것을 의미

 

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

- 이벤트 생성 주체 : 엔티티, 벨류, 도메인 서비스와 같은 도메인 객체, 상태가 바뀌면 관련 이벤트를 발생

- 이벤트 핼들러 : 이벤트 생성 주체가 발생한 이벤트에 반응함. 이벤트에 담긴 데이터를 이용하여 원하는 기능 실행

- 이벤트 디스패처 : 이벤트 생성 주체와 이벤트 핸들러를 연결 

 

### 이벤트 구성

이벤트는 발생한 이벤트에 대한 정보를 담아야 한다

- 이벤트 종류 ( 클래스 이름 ) 

- 이벤트 발생 시간

- 추가 데이터 ( 주문, 신규 변경 정보 등 이벤트 관련 정보)

이벤트 클래스는 과거 시제를 사용하는게 국룰

이벤트는 데이터를 담아야 하지만, 그렇다고 이벤트 자체와 관련 없는 데이터를 포함할 필요는 없다.

 

 

### 이벤트 용도

이벤트는 크게 두 용도로 사용한다.

1.. 트리거 : 도메인 상태 변경 후 후처리를 실행하기 위한 트리거로 이벤트를 사용

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

 

이벤트를 통해 서로 다른 도메인 로직을 섞이는 것을 방지할 수 있다. 

 

또한 기능 확장에도 용이하다. 해당 이벤트 구독하는 헨들러만 추가하면 된다.

 

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

### 디스패처 : ApplicationEventPublisher 이용

 

### Events : 이벤트 발행, ApplicationEventPublisher 이용

raise를 이용하여 이벤트 발생

 

### 이벤트 핸들러 : 이벤트 수신 처리

이벤트 처리시에는 @EventListener 사용

 

## 동기 이벤트 처리 문제

이벤트 처리를 할 때, 외부 서비스가 느리거나 예외가 발생하면 어떻게 할 것인가?

이벤트를 비동기로 처리하거나 이벤트와 트랜젝션을 연계하여 외부 시스템과 연동할 수 있다.

 

## 비동기 이벤트 처리

으리가 구현하는 것 중에서는 "A하면 이어서 B 하라"는  A하면 최대 언제까지 B를 해라" 라는 경우가 많다.

즉, 일정 시간 안에만 후속 조치를 처리하면 되는 경우가 많다.

B를 수행하는데 실패하면 일정 간격으로 재시도하거나, 수동으로 처리해도 되는 경우가 있다.

여기서 "A하면"은 이벤트로 볼 수 있다.

위와 같은 요구사항은 이벤트를 비동기로 처리하는 방식으로 구현할 수 있다.

 

이벤트를 비동기로 구현할 수 있는 방법은 다양하다

1. 로컬 핸들러를 비동기로 실행

2. 메시지큐 사용

3. 이벤트 저장소 & 이벤트 포워더

4. 이벤트 저장소 & 이벤트 제공 API

 

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

이벤트 핸들러를 별도 스레드로 실행한다

@Async를 사용하면 된다.

@EnableAsync 에너테이션을 사용해서 비동기 기능을 활성화한다

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

카프카나 rabbitMQ 같은 메시징 시스템을 사용한다.

이벤트가 발생하면 이벤트 디스페처는 이벤트를 메시지큐에 보낸다.

메시지큐는 메시지 리스너에게 전달한다.

메시지 리스너는 이벤트 핸들러를 이용해서 이벤트를 처리한다

필요하다면 이벤트를 발생하는 도메인 기능과 메시지큐에 이벤트를 저장하는 절차를 한 트랜젝션으로 묶어야

글로벌 트랜젝션을 사용하면 이벤트를 안전하게 전달할 수 있으나, 전체 성능이 떨어지고 카프카 같이 글로벌 트랜젝션을 지원 안하는 MQ도 있다.

 

동일 JVM에서 비동기 처리를 위해 MQ를 사용하는 것은 시스템을 복잡하게 만들 수 있다.

 

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

이벤트를 일단 DB에 저장한 뒤 별도 프로그램을 사용해서 이벤트 헨들러에 전달한다.

포워더는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행

포워더는 별도 스레드를 이용하기 때문에 이벤트 발행과 처리가 비동기로 처리

도메인의 상태와 이벤트 저장소가 동일한 DB를 사용해서 이벤트 실패시에는 포워더가 다시 이벤트 저장소에서 불러오면 된다.

@Scheduled를 이용하여 구현할 수 있다.

 

 

 

또한 이벤트 외부에 제공하는 API를 사용하는 것이다.

포워더 방식은 포워더로 이벤트를 외부에 전달한다면, API 방식은 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져간다.

포워더 방식은 이벤트를 어디까지 처리했는지 추적하는 역할이 포워더에 있다.

API 방식은은 외부 핸들러에게 있다.

offset을 이용해서 이벤트를 중복처리하지 않게 한다.

 

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

1. 이벤트 소스를 엔트리 안에 추가할 지 여부. 경우에 따라 이벤트 발생 주체에 대한 정보가 필요할 수도 있다.

2. 포워더에서 전송 실패를 얼마나 허용할 것인가? 재전송 횟수 제한이 필요한가? 계속 실패한다면 생략 가능한가?

3. 이벤트 손실되면 어떻게 하는가? 한 트랜젝션으로 처리해야 하는가?

4. 이벤트 순서를 보장해야 하는가? 순서를 보장해야 한다면 이벤트 저장소를 사용하는 것이 좋다

5. 이벤트 재처리가 가능한가? 멱등성을 제공할 것인가?(같은 이벤트를 중복 실행해도 결과가 같은가?)

 

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

''

위 경우레서 12번까지 다 성공했는데 13번에서 실패한다면?

 

 

12번 과정에서 실패한다면?

 

이벤트를 동기로 하든 비동기로 하든 이벤트 처리 실패와 트랜젝션 실패를 함께 고려해야 한다.

트랜젝션 실패와 이벤트 처리 실패를 모두 고려하면 복잡해지므로 경우의 수를 줄여야.

트랜젝션이 성공할 때만 이벤트를 실행하면 된다.

@TransactionalEventListener를 사용하면 스프링 트랜젝션 상태에 따라 이벤트 핸들러 호출 가능하다.

 

728x90

'스터디 > DDD 스터디' 카테고리의 다른 글

CQRS  (0) 2023.09.21
도메인 모델과 바운디드 컨텍스트  (0) 2023.09.02
애그리거트 트랜잭션 관리  (0) 2023.08.26
도메인 서비스  (0) 2023.08.18
응용 서비스와 표현 영역  (0) 2023.08.12