# Chapter 10: 이벤트

<br>

## 💡 책에서 기억하고 싶은 내용

<br>

### 시스템 간 강결합 문제

* 도메인 객체를 서비스에 전달하면 `설계상 문제`가 발생할 수 있다
  * 외부 서비스의 `성능`에 영향을 받는 문제
  * 도메인 객체에 서로 다른 도메인 로직이 `섞이는` 문제
* 바운디드 컨텍스트 간의 `강결합 (high coupling)` 을 없애기 위해 `이벤트` 를 사용한다
  * 특히 `비동기 이벤트` 를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다

### 이벤트 개요

* 이벤트란?
  * `과거에 벌어진 어떤 것` 을 의미한다
* 이벤트가 발생한다는 것은 `상태가 변경됐다` 는 것을 의미한다
* 이벤트는 발생하는 것에서 끝나지 않는다
  * 이벤트가 발생하면, 그 이벤트에 `반응` 하여 원하는 동작을 수행하는 기능을 구현한다
* `도메인 모델` 에서도 UI 컴포넌트와 유사하게 도메인의 `상태 변경` 을 이벤트로 표현할 수 있다
  * `~할 때`, `~가 발생하면`, `만약 ~하면` 과 같은 요구사항은 도메인의 상태 변경과 관련된 경우가 많고, 이런 요구 사항을 이벤트를 이용해서 구현할 수 있다

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

* 도메인 모델에 이벤트를 도입하려면 아래 4개의 구성요소를 구현해야 한다
  1. 이벤트
  2. 이벤트 생성 주체
  3. 이벤트 디스패처 (publisher)
  4. 이벤트 핸들러 (subscriber)
* 이벤트 관련 구성요소
  * `이벤트 생성 주체` — 이벤트 —> `이벤트 디스패처` — 이벤트 —> `이벤트 핸들러`
* `이벤트 생성 주체`
  * 도메인 모델에서 `이벤트 생성 주체` 는 엔티티, 밸류, 도메인 서비스와 같은 `도메인 객체` 이다
    * 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 `관련 이벤트를 발생` 시킨다
* `이벤트 핸들러` (subscriber)
  * `이벤트 핸들러` 는 이벤트 생성 주체가 발생한 이벤트에 반응한다
  * `이벤트 핸들러`는 생성 주체가 발생시킨 이벤트를 전달받아, 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다
* `이벤트 디스패처` (publisher)
  * 이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 `이벤트 디스패처` 다
  * 이벤트 생성 주체는 이벤트를 생성해서 디스페처에 이벤트를 전달한다
    * 이벤트를 전달받은 디스페처는 해당 이벤트를 처리할 수 있는 `핸들러`에 이벤트를 `전파`한다
  * 이벤트 디스패처의 구현 방식에 따라 이벤트 생성 / 처리를 `동기` 나 `비동기` 로 실행하게 된다

### 이벤트의 구성

* 이벤트는 `발생한 이벤트에 대한 정보`를 담으며, 다음을 포함한다
  1. 이벤트 종류
     * 클래스 이름으로 이벤트 종류를 표현
  2. 이벤트 발생 시간
  3. 추가 데이터
     * 이벤트와 관련된 정보
     * ex) 주문번호, 신규 배송지 정보
* 이벤트는 현재 기준으로 과거에 벌어진 것을 표현하기 때문에 이름에는 `과거 시제` 를 사용한다
* 이벤트는 이벤트 핸들러가 작업을 수행하는 데 필요한 데이터를 담아야 한다
  * `이벤트`가 `필요한 데이터`를 담고 있지 않으면, `이벤트 핸들러`는 리포지터리, 조회 API, 직접 DB 접근 등의 방식을 통해 필요한 데이터를 `조회` 해야한다
* 이벤트는 데이터를 담아야 하지만, 그렇다고 이벤트 자체와 필요 없는 데이터를 포함할 필요는 없다

### 이벤트 용도

* 이벤트는 크게 두 가지 용도로 쓰인다
  1. `트리거`
     * 도메인의 상태가 바뀔 때 다른 `후처리`가 필요하면, 후처리를 실행하기 위한 `트리거`로 이벤트를 사용할 수 있다
     * ex)
       * 주문 취소 시 환불 처리를 위한 주문 취소 이벤트
  2. 서로 다른 시스템 간의 `데이터 동기화`
     * ex)
       * 배송지를 변경하면 `외부 배송 서비스`에 바뀐 배송지 정보를 전송해야 한다
       * 주문 도메인은 배송지 변경 이벤트를 발생시키고, 이벤트 핸들러는 외부 배송 서비스와 배송지 정보를 `동기화` 할 수 있다

### 이벤트 장점

* 이벤트를 사용하면 서로 다른 도메인 로직이 `섞이는 것을 방지` 할 수 있다
  * ex)
    * 구매 취소 로직에 이벤트를 적용함으로써 환불 로직을 없앨 수 있다
      * 환불 실행 로직은 주문 취소 이벤트를 받는 `이벤트 핸들러` 로 이동하게 된다
    * 이벤트를 사용하여 주문 도메인에서 결제 (환불) 도메인으로의 `의존을 제거` 하게 된다
* 이벤트 핸들러를 사용하면 `기능 확장` 도 용이하다
  * ex)
    * 구매 취소 시 환불과 함께 이메일로 취소 내용을 보내고 싶다면, 이벤트 발송을 처리하는 핸들러를 구현하면 된다
    * 기능을 확장해도 구매 취소 로직은 수정할 필요가 없다

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

: 이벤트와 관련된 코드

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

### 이벤트 클래스

* 이벤트 자체를 위한 `상위 타입` 은 존재하지 않는다
  * 원하는 클래스를 이벤트로 사용하면 된다
* 이벤트 클래스의 `이름`
  * 이벤트는 과거에 벌어진 상태 변화나 사건을 의미하므로, 이벤트 클래스의 이름을 결정할 때에는 `과거 시제` 를 사용해야 한다
  * ex)
    * `OrderCanceledEvent`와 같이 클래스 이름 뒤에 `접미사` 로 `Event` 를 사용해서 이벤트로 사용하는 클래스라는 것을 명시적으로 표현하거나,
    * `OrderCanceled` 처럼 간결하게 과거 시제만 사용할 수도 있다
* 이벤트 클래스의 `구성`
  * 이벤트 클래스는 이벤트를 처리하는 데 필요한 `최소한의 데이터`를 포함해야 한다
  * 모든 이벤트가 `공통으로 갖는 프로퍼티` 가 존재한다면, 관련 `상위 클래스` 를 만들 수도 있다

### Events 클래스와 ApplicationEventPublisher

* 이벤트 발생과 출판을 위해 스프링이 제공하는 `ApplicationEventPublisher` 를 사용한다
  * `스프링 컨테이너` 는 `ApplicationEventPublisher` 도 된다
  * `Events` 클래스는 `ApplicationEventPublisher` 를 사용해서 이벤트를 `발생` 시킨다

### 흐름 정리

`응용 서비스` — 1 —>

```
                    `도메인` 

                             — 2 —> 

                                       `Events` 

                                               — 3 —> 

                                                  `ApplicationEventPublisher` 

                                                                                                         — 4 —>

                                                                                                                   `Event 핸들러`
```

1. 도메인 기능을 `실행`한다
2. 도메인 기능은 Events.raise() 를 이용해서 이벤트를 `발생`시킨다
3. Events.raise() 는 스프링이 제공하는 ApplicationEventPublisher를 이용해서 이벤트를 출판한다
4. ApplicationEventPublisher는 @EventListener(이벤트타입.class) annotation이 붙은 method를 찾아 실행한다

### 동기 이벤트 처리 문제

* 외부 서비스에 영향을 받는 문제
  * 외부 서비스의 성능 저하가 시스템의 성능 저하로 연결되는 경우
* 외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능과 트랜잭션 범위 문제 해소 방법
  1. 이벤트를 `비동기` 로 처리
  2. 이벤트와 `트랜잭션` 을 연계

### 비동기 이벤트 처리

* `A 하면 이어서 B 하라` 는 요구사항 중 `A 하면 최대 언제까지 B 하라` 로 바꿀 수 있는 요구사항은 이벤트를 `비동기` 로 처리하는 방식으로 구현할 수 있다
  * 즉, A 이벤트가 발생하면 `별도 스레드`로 B를 수행하는 핸들러를 실행하는 방식으로 요구사항을 구현할 수 있다
* 이벤트로 비동기를 구현하는 방법
  1. 로컬 핸들러를 비동기로 실행하기
  2. 메시지 큐를 사용하기
  3. 이벤트 저장소와 이벤트 포워더 사용하기
  4. 이벤트 저장소와 이벤트 제공 API 사용하기

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

* 이벤트 핸들러를 비동기로 실행하는 방법은 이벤트 핸들러를 `별도 스레드` 로 실행하는 것이다
* 스프링이 제공하는 `@Async` annotation을 사용하면 비동기로 이벤트 핸들러를 실행할 수 있다
  1. `@EnableAsync` annotation을 사용해서 비동기 기능을 활성화 한다
     * 스프링 설정 클래스 (`@SpringBootApplication`) annotation이 붙는 클래스에 `@EnableAsync` annotation을 붙인다
  2. 이벤트 핸들러 메서드에 `@Async` annotation을 붙인다
     * 비동기로 실행할 이벤트 핸들러 method에 `@Async` annotation을 붙인다

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

* `Kafka`나 `RabbitMQ`와 같은 메시징 시스템을 이용하는 것
* 이벤트가 발생하면 이벤트 디스패처는 이벤트를 `메시지 큐` 에 보낸다
  * 메시지 큐는 이벤트를 `리스너` 에 전달하고,
  * 메시지 리스너는 알맞은 `이벤트 핸들러` 를 이용해서 이벤트를 처리한다
* 이벤트를 메시지 큐에 저장하는 과정과 메시지 큐에서 이벤트를 읽어와 처리하는 과정은 별도 thread나 process로 처리된다
* 필요하다면 이벤트를 발생시키는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 `한 트랜잭션`으로 묶어야 한다
  * 도메인 기능을 실행한 결과를 DB에 반영하고, 이 과정에서 발생한 이벤트를 메시지 큐에 저장하는 것을 같은 트랜잭션 범위에서 실행하려면 `글로벌 트랜잭션` 이 필요하다
* 메시지 큐를 사용하면 보통 이벤트를 발생시키는 주체와 이벤트 핸들러가 별도 프로세스에서 동작한다
  * `이벤트 발생 JVM`과 `이벤트 처리 JVM`이 다르다는 것을 의미한다
  * 한 JVM에서 이벤트 발생 주체와 이벤트 핸들러가 메시지 큐를 이용해서 이벤트를 주고받을 수 있지만, 동일 JVM에서 비동기 처리를 위해 메시지 큐를 사용하는 것은 시스템을 복잡하게 만들 뿐이다
* `RabbitMQ` 처럼 많이 사용되는 메시징 시스템은 `글로벌 트랜잭션` 지원과 함께 `클러스터 고가용성` 을 지원하기 때문에 안정적으로 메시지를 전달할 수 있는 장점이 있다
* `Kafka` 는 글로벌 트랜잭션을 지원하진 않지만, 다른 메시징 시스템에 비해 높은 성능을 보여준다

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

* `포워더 방식`
  * 이벤트를 일단 DB에 저장한 뒤에, 별도 프로그램(포워더)을 이용해서 이벤트 핸들러에 전달하는 것이다
    * `포워더`는 이벤트를 주기적으로 읽어와 전달하며, 어디까지 전달했는지 추적한다
  * 이벤트가 발생하면 핸들러는 `스토리지`에 이벤트를 저장한다
    * `포워더` 는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 `실행` 한다
    * `포워더` 는 `별도 스레드` 를 이용하기 때문에 이벤트 `발행` 과 `처리` 가 비동기로 처리된다
  * 이 방식은 도메인의 상태와 이벤트 저장소로 `동일한 DB` 를 사용한다
    * 즉, 도메인의 상태 변화와 이벤트 저장이 `로컬 트랜잭션` 으로 처리된다
  * 이벤트를 `물리적 저장소` 에 저장하기 때문에 이벤트 처리에 `실패` 할 경우, 포워더는 다시 이벤트 저장소에서 이벤트를 읽어와 핸들러를 `실행` 하면 된다
  * `포워더 구현`
    * 일정 주기로 \`EventStore에서 이벤트를 읽어와 이벤트 핸들러에 전달한다
    * API 방식 client와 마찬가지로, 마지막으로 전달한 이벤트의 offset을 기억해 두었다가 다음 조회 시점에 마지막으로 처리한 offset 부터 이벤트를 가져오면 된다
* `API 방식`
  * API 방식과 포워더 방식의 차이는 이벤트를 전달하는 방식에 있다
    * 포워더 방식이 `포워더` 를 이용해서 이벤트를 외부에 전달한다면,
    * API 방식은 `외부 핸들러` 가 API 서버를 통해 이벤트 목록을 가져간다
  * 포워더 방식은 이벤트를 어디까지 처리했는지 추적하는 역할이 포워더에 있다면,
    * API 방식에서는 이벤트 목록을 요구하는 외부 핸들러가 자신이 어디까지 이벤트를 처리했는지 기억해야 한다
  * API를 사용하는 client는 일정 간격으로 다음 과정을 실행한다 1. 가장 마지막에 처리한 데이터의 `offset` 인 `lastOffset` 을 구한다 - 저장한 `lastOffset`이 없으면 0을 사용한다 2. 마지막에 처리한 `lastOffset`을 offset으로 사용해서 API를 실행한다 3. API 결과로 받은 데이터를 처리한다 4. offset + 데이터 개수를 `lastOffset`으로 저장한다 - 마지막에 처리한 `lastOffset` 을 저장하는 이유 - 같은 이벤트를 `중복` 해서 처리하지 않기 위해!
  * 클라이언트 API를 이용해서 언제든지 원하는 이벤트를 가져올 수 있기 때문에 이벤트 처리에 `실패` 하면 다시 실패한 이벤트로부터 읽어와 이벤트를 `재처리` 할 수 있다
    * API 서버에 `장애`가 발생한 경우에도 주기적으로 `재시도` 를 해서 API 서버가 살아나면 이벤트를 `처리`할 수 있다

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

* 이벤트 소스를 `EventEntry`에 추가할지 여부
  * `EventEntry` 는 이벤트 발생 주체에 대한 정보를 갖지 않는다
  * 만약 특정 주체가 발생시킨 이벤트만 조회하는 기능을 구현하려면 이벤트에 `발생 주체 정보` 를 추가해야 한다
* `포워더` 에서 전송 실패를 얼마나 허용할 것인지?
  * 포워더는 이벤트 전송에 실패하면 실패한 이벤트로부터 다시 읽어와 전송을 시도한다
  * but, 특정 이벤트에서 계속 전송에 실패하면 그 이벤트 때문에 나머지 이벤트를 전송활 수 없게 된다
  * 따라서 포워더를 구현할 때는 실패한 이벤트의 `재전송 횟수 제한` 을 두어야 한다
    * ex)
      * 동일 이벤트 3회 실패 시 해당 이벤트 생략 및 다음 이벤트로 넘어가기
      * 또는 처리에 실패한 이벤트를 생략하지 않고 별도 실패용 DB나 메시지 큐에 저장하기
        * 이후 실패 분석이나 후처리에 도움이 된다
* `이벤트 손실`
  * 이벤트 저장소를 사용하면 이벤트 발생과 이벤트 저장을 한 트랜잭션으로 관리하기 때문에 트랜잭션에 성공하면 이벤트가 저장소에 보관되는 것을 보장할 수 있지만,
  * 로컬 핸들러를 이용해서 이벤트를 비동기로 처리할 경우 이벤트 처리에 실패하면 이벤트를 `유실` 하게 된다
* `이벤트 순서`
  * 이벤트 발생 순서대로 외부 시스템에 전달해야 할 경우, `이벤트 저장소` 를 사용하는 것이 좋다
    * 이벤트 저장소는 저장소에 이벤트를 발생 순서대로 저장하고 그 순서대로 이벤트 목록을 제공하기 때문!
    * but, 메시징 시스템은 사용 기술에 따라 이벤트 전달 순서와 메시지 전달 순서가 다를 수 있다
* `이벤트 재처리`
  * 가장 쉬운 방법은 마지막으로 처리한 이벤트의 순번을 기억해 두었다가, 이미 처리한 순번의 이벤트가 도착하면 해당 이벤트를 처리하지 않고 무시하는 것이 있다
  * 또는 이벤트를 `멱등` 으로 처리하는 방법도 있다
    * 이벤트 핸들러가 `멱등성` 을 가지면 시스템 장애로 인해 같은 이벤특 중복해서 발생해도 결과적으로 동일한 상태가 된다
    * 이는 이벤트 중복 발생이나 중복 처리에 대한 부담을 줄여준다

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

* 이벤트 처리를 동기로 하든 비동기로 하든 `이벤트 처리 실패`와 `트랜잭션 실패`를 함께 고려해야 한다
* 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하는 방법
  * 스프링의 `@TransactionalEventListener` annotation 사용하기
    * 스프링 트랜잭션 상태에 따라 이벤트 핸들러를 실행할 수 있게 한다
  * 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하게 되면, `트랜잭션 실패` 에 대한 경우의 수가 줄어들어 이벤트 처리 실패만 고려해야 한다
* 이벤트 특성에 따라 `재처리 방식` 을 결정하면 된다
