6장 비즈니스 로직 개발 이벤트 소싱
목차
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
6장 비즈니스 로직 개발: 이벤트 소싱
6.1 이벤트 소싱 응용 비즈니스 로직 개발
__6.1.1 기존 영속화의 문제점
__6.1.2 이벤트 소싱 개요
__6.1.3 동시 업데이트: 낙관적 잠금
__6.1.4 이벤트 소싱과 이벤트 발행
__6.1.5 스냅샷으로 성능 개선
__6.1.6 멱등한 메시지 처리
__6.1.7 도메인 이벤트 발전시키기
__6.1.8 이벤트 소싱의 장점
__6.1.9 이벤트 소싱의 단점
6.2 이벤트 저장소 구현
__6.2.1 이벤추에이트 로컬 이벤트 저장소의 작동 원리
__6.2.2 자바용 이벤추에이트 클라이언트 프레임워크
6.3 사가와 이벤트 소싱을 접목
__6.3.1 코레오그래피 사가 구현: 이벤트 소싱
__6.3.2 오케스트레이션 사가 생성
__6.3.3 이벤트 소싱 기반의 사가 참여자 구현
__6.3.4 사가 오케스트레이터 구현: 이벤트 소싱
6.4 마치며
정리 전략?
- 전부 정리는 NO
- 핵심중에서 꼭 적고 가야겠다 싶은 것만!
- 나의 생각을 섞어서 적을것..!
- 떠오르는 것 먼저 간단하게..!
6장 비즈니스 로직 개발: 이벤트 소싱 6.1 이벤트 소싱 응용 비즈니스 로직 개발 **6.1.1 기존 영속화의 문제점 **6.1.2 이벤트 소싱 개요 **6.1.3 동시 업데이트: 낙관적 잠금 **6.1.4 이벤트 소싱과 이벤트 발행 **6.1.5 스냅샷으로 성능 개선 **6.1.6 멱등한 메시지 처리 **6.1.7 도메인 이벤트 발전시키기 **6.1.8 이벤트 소싱의 장점 **6.1.9 이벤트 소싱의 단점 6.2 이벤트 저장소 구현 **6.2.1 이벤추에이트 로컬 이벤트 저장소의 작동 원리 **6.2.2 자바용 이벤추에이트 클라이언트 프레임워크 6.3 사가와 이벤트 소싱을 접목 **6.3.1 코레오그래피 사가 구현: 이벤트 소싱 **6.3.2 오케스트레이션 사가 생성 **6.3.3 이벤트 소싱 기반의 사가 참여자 구현 __6.3.4 사가 오케스트레이터 구현: 이벤트 소싱 6.4 마치며
6장 비즈니스 로직 개발: 이벤트 소싱
- 여기서 말하는 이벤트 소싱이란, 일련의 이벤트의 발생을 DB에 저장하는 방식을 의미했던 것으로 기억
- 보통의 개발은 현재 상태만 저장을 하는데, 이벤트 소싱은 과정을 저장한다고 볼수있음.
6.1 이벤트 소싱 응용 비즈니스 로직 개발
- 이벤트 : 애그리거트의 상태변화를 의미.
- 상태 변화를 나타내는 일련의 도메인 이벤트로 애그리거트를 저장한다.
**6.1.1 기존 영속화의 문제점
- 기존 영속화는 과정에 대한 정보가 없다는 것이 문제.
- 개발자가 수동으로 로그 테이블이나 로그를 남기지 않는 이상 추적이 불가능함.
- https://chatgpt.com/share/66e789ba-acc0-8011-8ff0-53393229f256
**6.1.2 이벤트 소싱 개요
- 앞서 말한듯.

- 애그리거트를 로드할 떄 이벤트 저장소에서 이벤트를 가져와 재연을 하는데, 구체적으로 3단계로 구성이 된다.
- 애그리거트의 이벤트를 로드한다.
- 기본 생성자를 호출하여 애그리거트 인스턴스를 생성한다.
- 이벤트를 하나씩 순회하여 apply()를 호출한다.
- Event Sourcing은 데이터 저장 방식 중 하나로, 시스템 내에서 발생하는 모든 “이벤트(event)”를 그대로 저장하여 시스템의 상태를 재현하거나 복구하는 방법을 말합니다. 즉, 시스템의 상태를 현재의 데이터로만 유지하는 것이 아니라, 그 상태에 도달하게 한 모든 변화를 “이벤트”로 저장하고, 이를 통해 언제든지 과거의 특정 시점으로 돌아가거나 상태를 재구성할 수 있습니다.
Event Sourcing의 핵심 개념:
- 이벤트(event): 시스템에서 일어난 중요한 변화나 동작을 기록한 단위입니다. 예를 들어, 사용자가 상품을 구매했다는 것도 이벤트로 기록됩니다.
- 이벤트 저장소(event store): 이벤트는 데이터베이스에 저장되며, 기존 상태를 덮어쓰지 않고 새로운 이벤트가 추가되는 방식입니다. 이렇게 저장된 이벤트는 추후에 재생(replay)하여 시스템 상태를 복원하거나 분석할 수 있습니다.
- 상태 복원(state rehydration): 시스템의 현재 상태는 이벤트를 순차적으로 처리하여 재구성할 수 있습니다. 이때 모든 이벤트를 차례대로 재생함으로써 특정 시점의 상태를 얻습니다.
왜 Event Sourcing이라 부르는가?
- “Event”는 시스템의 변화나 상태 변화를 나타내는 이벤트를 의미합니다.
- “Sourcing”은 이벤트를 근원(source)으로 삼아 데이터를 관리하고, 시스템의 상태를 재구성한다는 의미입니다. 즉, 이벤트를 시스템의 주요 데이터 소스로 사용하기 때문에 “Event Sourcing”이라고 불립니다.
Event Sourcing을 사용하는 이유:
- 상태 추적 및 복구: 이벤트를 통해 시스템의 모든 변화를 추적하고 과거로 돌아가거나 특정 시점으로 상태를 복구할 수 있습니다.
- 감사와 감사 기록: 모든 이벤트가 기록되므로, 시스템에서 어떤 일이 발생했는지 명확히 알 수 있어 감사(audit) 용도로 유용합니다.
- 이력 관리: 이벤트 기록을 통해 모든 상태 변화의 이력을 관리할 수 있어 데이터를 시간적으로 분석할 수 있습니다.
- 비동기 처리: 이벤트 기반으로 시스템을 분산 처리하거나 비동기적으로 처리하기 용이합니다.
하지만 Event Sourcing은 복잡도가 증가하고 이벤트 저장소의 크기가 커질 수 있다는 단점도 있기 때문에, 신중하게 사용해야 합니다.
이벤트는 곧 상태 변화
애그리거트의 관심사는 오직 이벤트
- 이벤트 소싱은 커맨드 메서드 하나를 둘 이상의 메서드로 리팩토링한다.
- 애그리거트는 다음 순서대로 생성된다.
- 기본 생성자로 애그리거트 루트를 초기화
- process()를 호출하여 새 이벤트를 발생
- 새 이벤트를 하나씩 순회하면서 apply()를 호출하여 애그리거트를 업데이트
- 이벤트 저장소에 새 이벤트를 저장한다.
- 또한 애그리거트는 다음 순서대로 업데이트 된다.
- 이벤트 저장소에서 애그리거트 이벤트를 로그한다.
- 기본 생성자로 애그리거트 루트를 초기화한다.
- 가져온 이벤트를 하나씩 순회하며 애그리거트 루트의 apply()를 호출한다.
- process()를 호출하여 새 이벤트를 발생시킨다.
- 새 이벤트를 순회하면서 apply()를 호출하여 애그리거트를 업데이트한다.
- 이벤트 저장소에 새 이벤트를 저장한다.
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
// 예제 6-1 Order 애그리거트 필드와 인스턴스 초기화 메서드 (애그리거트 생성)
public class Order {
private OrderState state;
private Long consumerId;
private Long restaurantId;
private OrderLineItems orderLineItems;
private DeliveryInformation deliveryInformation;
private PaymentInformation paymentInformation;
private Money orderMinimum;
public Order() {
}
public List<Event> process(CreateOrderCommand command) { // 커맨드 검증 후 OrderCreatedEvent 반환
return events(new OrderCreatedEvent(command.getOrderDetails()));
}
public void apply(OrderCreatedEvent event) { // Order 필드를 초기화해서 OrderCreatedEvent 적용
OrderDetails orderDetails = event.getOrderDetails();
this.orderLineItems = new OrderLineItems(orderDetails.getLineItems());
this.orderMinimum = orderDetails.getOrderMinimum();
this.state = CREATE_PENDING;
}
**6.1.3 동시 업데이트: 낙관적 잠금
- 너무 익숙한 개념. 패스~
**6.1.4 이벤트 소싱과 이벤트 발행
- 이벤트 발행: 폴링
- 폴링: 하나의 장치(또는 프로그램)이 충돌 회피 또는 동기화 처리 등을 목적으로 다른 장치(또는 프로그램)의 상태를 주기적으로 검사하여 일정한 조건을 만족할 때 송수신 등의 자료처리를 하는 방식

- 이러한 문제를 해결하는 방법은 EVENT 테이블에 이벤트 발행 여부를 추적할 수 있는 컬럼을 추가하는 것이다. SELECT * FROM EVENTS WHERE PUBLISHED = 0 ORDER BY EVENT_ID ASC 쿼리로 미발행 이벤트를 검색 메시지 브로커에 이벤트를 발행 UPDATE EVENTS SET PUBLISHED = 1 WHERE EVENT_ID = ? 이벤트가 발행된 것으로 표시
- 폴링: 하나의 장치(또는 프로그램)이 충돌 회피 또는 동기화 처리 등을 목적으로 다른 장치(또는 프로그램)의 상태를 주기적으로 검사하여 일정한 조건을 만족할 때 송수신 등의 자료처리를 하는 방식
- 이벤트 발행: 트랜잭션 로그 테일링

- https://velog.io/@eastperson/Transaction-Outbox-Pattern-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0#%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%A1%9C%EA%B7%B8-%ED%85%8C%EC%9D%BC%EB%A7%81-%ED%8C%A8%ED%84%B4transaction-log-tailing-pattern
**6.1.5 스냅샷으로 성능 개선
- 과정을 저장하는 방식의 단점은 현재 상태를 보기 위해서는 과정에 대하는 기록을 읽고 모두 연산해서 현재 상태를 알아내야한다는 단점이 있다.
- 즉 history가 길면, 성능이 안좋을수 있다.
- 이것을 적당한 시점에 스냅샷을 저장함으로써 일부 해소 가능하다는 이야기.

**6.1.6 멱등한 메시지 처리
- 멱등성은 동일한 인풋이 여러번 주어져도, 영향이 똑같은 것을 의미한다.
- 발행된 이벤트를 처리하는 컨슈머도, 중복된 이벤트가 발생했을때, 중복을 인지하고, 그것에 적절한 대응을 해주거나(아무것도 안하는 것이 방법일듯) 아무것도 하지 않아야한다.
- 또는 중복을 솎아내는 작업이 필요하더라~, 이 이야기인거임.
- rdbms 이벤트 저장소 사용
- 이벤트는 events 테이블에 삽입하는 트랜잭션의 일부로 삽입하면 됨.
nosql 이벤트 저장소로 사용
이 부분에서 설명하고 있는 상황은 다음과 같습니다.
메시지 처리의 멱등성: 메시지가 여러 번 전달되더라도 애그리거트(Aggregate) 상태가 일관되게 유지되도록 해야 합니다. 이를 위해 메시지 컨슈머가 이미 처리한 메시지를 다시 처리하지 않도록 메시지 ID를 기록하고, 중복 메시지는 무시해야 합니다.
문제 상황: 메시지를 처리했을 때, 그 결과로 애그리거트가 업데이트되지 않거나, 아무런 이벤트도 생성되지 않는 경우가 있습니다. 이런 경우에도 메시지 자체는 이미 처리된 것으로 간주해야 하지만, 기록이 없으면 이후에 같은 메시지가 다시 도착했을 때 이를 재처리하게 됩니다. 이 과정에서 예기치 않은 상태 변경이 발생할 수 있습니다.
- 예를 들어, 메시지 A를 처음 처리했을 때 아무런 이벤트도 발생하지 않아서 기록이 없습니다.
- 이후에 메시지 B가 도착해서 정상적으로 애그리거트를 업데이트하고 이벤트가 발생합니다.
- 시간이 지나서 메시지 A가 재전달되면, 이전 처리 기록이 없으므로 메시지 A가 다시 처리되고 애그리거트가 업데이트될 수 있습니다.
- 이로 인해 애그리거트의 상태가 불일치하거나 원치 않은 상태 변경이 발생할 수 있습니다.
해결 방법: 항상 이벤트를 발행하는 것이 핵심입니다. 메시지를 처리할 때 애그리거트가 변경되지 않더라도, 그 사실을 기록하기 위해 “가짜 이벤트”를 생성합니다. 이 가짜 이벤트는 실제로 애그리거트 상태를 변경하지 않지만, 메시지가 처리되었다는 사실을 기록하는 역할을 합니다. 나중에 동일한 메시지가 다시 전달되더라도 이 기록을 보고 이미 처리된 메시지임을 인식하고 무시할 수 있습니다.
- 즉, 메시지가 처리되었음을 표시하는 이벤트(가짜 이벤트)를 기록하고, 실제로 애그리거트 상태를 변경하는 이벤트는 별도로 관리하는 구조입니다.
- 이벤트 컨슈머는 이 가짜 이벤트를 인지하고 무시하게 설계됩니다. 즉, 이 가짜 이벤트는 이벤트 소비자들에게 의미가 없고, 단지 중복 처리를 방지하는 용도로만 사용됩니다.
이렇게 하면, 메시지 처리 결과 이벤트가 생성되지 않는 경우에도 메시지가 처리되었다는 기록을 남길 수 있으므로 이후 재처리되는 문제를 방지할 수 있습니다.
**6.1.7 도메인 이벤트 발전시키기
- 이벤트 스키마
- 이벤트 소싱에 기반한 애플리케이션의 스키마는 아래의 세 가지로 구성된다.
- 하나 이상의 애그리거트로 구성
- 각 애그리거트가 발생시키는 이벤트를 정의
- 이벤트 구조를 정의
- 업캐스팅을 통한 스키마 변환 관리
- DB 스키마 변경은 보통 마이그레이션을 이용하여 처리. 즉, 변경된 새 스키마에 기존 데이터를 옮겨 담는 SQL 스크립트를 실행해서 옮김. 스키마 마이그레이션은 버전 관리 시스템에 저장하고 플라이웨이 등의 툴을 써서 DB에 반영한다.
- 이벤트 소싱 애플리케이션도 하위 호환이 안 되는 변경을 비슷한 방법으로 처리할 수 있지만, 이벤트 소싱 프레임워크가 이벤트 저장소에서 이벤트를 로드할 때 바꾸어 준다. 보통 업캐스터라고 하는 컴포넌트가 개별 이벤트를 구 버전에서 신 버전으로 업데이트하므로 애플리케이션 코드는 현재 이벤트 스키마를 잘 처리하기만 하면 된다.
**6.1.8 이벤트 소싱의 장점
- 장점
- 도메인 이벤트를 확실하게 발행
- 애그리거트 상태가 변경될 때마다 확실히 이벤트를 발행
- 애그리거트 이력 보존
- 애그리거트마다 전체 이력이 그대로 보존되고, 애그리거트 과거 상태를 임시 쿼리로 쉽게 조회할 수 있다. 예를 들어 어떤 고객이 과거 특정 시점에 신용 한도가 얼마였는지 쉽게 계산할 수 있음.
- O/R 임피던스 불일치 문제를 거의 방지
- 이벤트 소싱은 이벤트를 취합하는 대신 저장한다. 이벤트는 보통 쉽게 직렬화할 수 있는 단순한 구조. 서비스는 과거 상태를 죽 나열해서 복잡한 애그리거트 스냅샷을 뜰 수 있음. 즉, 애그리거트와 애그리거트를 직렬화한 표현형 사이를 한 수준 더 간접화(indirection)할수 있다. 개발자에게 타임머신 제공
**6.1.9 이벤트 소싱의 단점
- 단점
- 새로운 프로그래밍 모델을 배우려면 시간이 걸린다.
- 메시징 기반 애플리케이션의 복잡함.
- 메시지 브로커가 적어도 1회 이상 전달하기 때문에 멱등하지 않은 이벤트 핸들러는 중복 이벤트를 감지해 걸러내야 한다.
- 이벤트를 발전시키기 어렵다.
- 이벤트 스키마가 발전이 됨으로써 생기는 문제. 이벤트는 영구저장 되므로 애그리거트는 각 스키마 버전별로 이벤트를 폴드해야하는데, 그러다 보면 버전마다 분기 처리하는 코드로 애그리거트가 도배될 가능성이 있따. 이 문제를 해결하는 방법은 이벤트를 이벤트 저장소에서 가져올 때 최신 버전으로 업그레이드 하는 것(6.1.7). 그러면 애그리거트에 있는 이벤트를 업그레이드하는 코드를 따로 분리할 수 있고, 애그리거트는 최신 버전의 이벤트만 적용하면 된다.
- 데이터를 삭제하기 어렵다
- 보통 데이터는 소프트 삭제를 사용한다. 이에 따라서 발생하는 회원의 개인정보 관련 이슈가 발생할 수 있음.
- 이를 해결하기 위해 개인정보는 암호화를 한 뒤 DB에 저장한다.
- 이벤트 저장소를 쿼리하기 어렵다.
- 신용 한도가 소진된 고객을 찾는다고 가정. 신용 한도를 가리키는 컬럼은 따로 없기 때문에 SELECT * FROM CUSTOMER WHERE CREDIT_LIMIT = 0 같은 쿼리는 사용할 수 없다.
- 그래서 처음에 신용 한도를 설정한 이후 업데이트한 이벤트를 폴드해서 신용 한도를 계산할 수 밖에 없는데, SELECT 문이 중첩된 매우 복잡하고 비효율적인 쿼리를 사용하게 됨. 심지어 NoSQL 이벤트 저장소는 대부분 기본키 검색만 지원하므로 CQRS 방식으로 쿼리를 지원해야 한다.
6.2 이벤트 저장소 구현
**6.2.1 이벤추에이트 로컬 이벤트 저장소의 작동 원리
이벤추에이트 로컬의 이벤트 DB 스키마
- events: 이벤트를 저장 entities: 엔티티당 로우 하나 snapshots: 스냅샷을 저장
- create table events( event_id varchar(1000) PRIMARY KEY, event_type varchar(1000) NOT NULL, event_data varchar(1000) NOT NULL, entity_type varchar(1000) NOT NULL, entity_id varchar(1000) NOT NULL, triggering_event varchar(1000) ); triggering_event는 중복 이벤트/메시지를 발견하는 용도의 컬럼. 이벤트를 생성한 메시지/이벤트의 ID를 여기에 저장한다.
- create table entities( entity_type varchar(1000), entity_id varchar(1000), entity_version varchar(1000) NOT NULL, PRIMARY KEY(entity_type, entity_id) ); entities 테이블은 엔티티별 현재 버전을 저장. 낙관적 잠금을 구현하는 용도로 쓰인다. 엔티티가 생성되면 이 테이블에 한 로우가 삽입되고, 엔티티가 업데이트될 때마다 entity_version 컬럼도 업데이트 된다.
- create table snapshots( entity_type varchar(1000), entity_id varchar(1000), entity_version varchar(1000) NOT NULL, snapshot_type varchar(1000) NOT NULL, snapshot_json varchar(1000) NOT NULL, triggering_event varchar(1000) PRIMARY KEY(entity_type, entity_id, entity_version) ); 스냅샷 엔티티는 entity_type과 entity_id 컬럼으로 지정한다. snopshot_json 컬럼은 스냅샷을 직렬화한 표현형이고 snapshot_type은 그 타입을 가리킨다.
- 이 스키마는 find(), create(), update() 3개의 작업을 지원한다. find(): snapshot 테이블에서 가장 최근 스냅샷을 조회한 후, 스냅샷이 존재하면 events 테이블을 뒤져 event_id가 스냅샷의 entity_version보다 크거나 같은 이벤트를 모두 찾고, 스냅샷이 존재하지 않으면 주어진 엔티티의 이벤트를 모두 조회한다. 또 entity 테이블에서 엔티티 현재 버전을 가져온다. create(): entities 테이블에 새 로우를 삽입하고 events 테이블에는 이벤트를 삽입한다. update(): events 테이블에 이벤트를 삽입한다. 그 다음 UPDATE 문으로 entities 테이블에 있는 엔티티 버전을 업데이트해서 낙관적 잠금 체크를 수행한다.
- 서비스는 카프카로 구현된 이벤트 브로커를 구독해서 이벤트를 소비한다
이벤추에이트 로컬의 이벤트 브로커를 구독하여 이벤트를 소비
- 서비스는 카프카로 구현된 이벤트 브로커를 구독해서 이벤트를 소비한다.
이벤추에이트 로컬 이벤트 릴레이가 이벤트를 DB에서 메시지 브로커로 전파
- 이벤트 릴레이: DB에 삽입된 이벤트를 이벤트 브로커로 전파. 보통 트랜잭션 로그 테일링을 이용하고 다른 DB를 폴링하기도 한다.
**6.2.2 자바용 이벤추에이트 클라이언트 프레임워크
애그리거트 정의: ReflectiveMutableCommandProcessingAggregate 클래스
1
2
3
4
5
6
// 예제 6-3 이벤추에이트 버전의 Order 클래스
public class Order extends ReflectiveMutableCommandProcessingAggregate<Order, OrderCommand>{
public List<Event> process(CreateOrderCommand command) { ... }
public void apply(OrderCreatedEvent event) { ... }
}
두 타입 매개변수 (애그리거트 구상 클래스, 애그리거트 커맨드 클래스의 상위 클래스)를 받는 제네릭 클래스 리플렉션을 이용하여 커맨드 및 이벤트를 적절한 메서드에 디스패치한다. 커맨드는 process()에, 이벤트는 apply()에 각각 디스패치된다.
애그리거트 커맨드 정의
1
2
public interface OrderCommand extends Command{
}
public class CreateOrderCommand implements OrderCommand { … } 애그리거트 커맨드 클래스는 주어진 애그리거트 의 기초 인터페이스를 상속해야 한다. 그래서 Order 애그리거트 커맨드도 위와 같이 OrderCommand를 상속한다. OrderCommand 인터페이스는 Command 인터페이스를 상속하며, OrderCommand 인터페이스는 CreateOrderCommand 커맨드 클래스가 구현된다.
도메인 이벤트 정의
1
2
interface OrderEvent extends Event{
}
public class OrderCreated implements OrderEvent { … } 애그리거트 이벤트 클래스는 메서드가 하나도 없는 마커 인터페이스 Event를 상속 이벤트 클래스 OrderCreated는 Order 애그리거트의 이벤트 클래스용 기초 인터페이스 OrderEvent를 구현한다. OrderEvent는 다시 Event를 상속한다.
AggregateRepository 클래스로 애그리거트 생성, 검색, 수정
AggregateRepository는 애그리거트 클래스, 애그리거트의 기초 커맨드 클래스를 타입 매개변수로 받는 제네릭 클래스. 이 클래스에는 다음 세 메서드가 오버로드 되어있다. save(): 애그리거트를 생성한다. save()는 커맨드 객체를 매개변수로 받아 다음과 같은 일을 한다. 기본 생성자로 애그리거트 인스턴스를 만든다. process()를 호출하여 커맨드를 처리한다. apply()를 호출하여 생성된 이벤트를 적용한다. 생성된 이벤트를 이벤트 저장소에 저장한다. find(): 애그리거트를 검색한다. update(): 애그리거트를 수정한다.
도메인 이벤트 구독
1
2
3
4
5
6
7
8
9
10
11
12
// 예제 6-5 CreditReservedEvent 핸들러
@EventSubscriber(id="orderServiceEventHandlers")
public class OrderServiceEventHandlers{
@EventHandlerMethod
public void creditReserved(EventHandlerContext<CreditReserved> ctx{
CreditReserved event = ctx.getEvent();
}
}
// @EventSubscriber로 이벤트를 처리할 스프링 빈을 저장한다.
// @EventHandlerMethod는 creditReserved()를 이벤트 핸들러로 식별하는 어노테이션.
이벤트 핸들러는 이벤트 및 관련 메타데이터가 포함된 EventHandelerContext형 매게변수를 받는다.
6.3 사가와 이벤트 소싱을 접목
**6.3.1 코레오그래피 사가 구현: 이벤트 소싱
- 이벤트 소싱과 코레오그래피 사가는 찰떡궁합. 이벤트 소싱은 메시징 기반의 IPC, 메시지 중복 제거, 원자적 상태 업데이트와 메시지 전송 등 사가가 필요로 하는 여러가지 메커니즘을 제공한다.
- 하지만 사가 코레오그래피에 이벤트를 사용하면 이벤트의 목적이 이원화가 된다. 이벤트 소싱은 상태 변화를 나타내기 위해 이벤트를 이용하는데, 이벤트를 사가 코레오그래피에 갖다 쓰면 애그리거트는 상태 변화가 없어도 무조건 이벤트를 발생시켜야 한다.
- 애그리거트를 업데이트하면 비즈니스 규칙에 위배될 경우 애그리거트는 반드시 이벤트를 발생시켜 오류를 보고해야 한다. 더 큰 문제는 사가 참여자가 애그리거트를 생성할 수 없을 경우다. 에러 이벤트를 발생시킬 애그리거트가 하나도 없기 때문이다.
- 이러한 문제가 있어서 복잡하더라도 오케스트레이션 사가를 구현하는 것이 최선이다.
**6.3.2 오케스트레이션 사가 생성
- 사가 오케스트레이터는 일부 서비스 메서드에 의해 생성된다. 가령 OrderService.createOrder() 같은 서비스는 애그리거트를 생성/수정하고 사가 오케스트레이터를 생성하는 두 가지 일을 한다.
- 서비스는 이 두 가지 액션을 첫 번째 액션이 수행되면 두 번째 액션은 최종적으로 실행되는 방식으로 수행한다. 두 액션이 서비스에서 반드시 수행되도록 보장하는 방법은 이벤트 저장소의 종류마다 다르다.
- 주문 서비스가 Order 애그리거트를 만들어 이벤트 저장소에 저장하면, 이벤트 저장소는 OrderCreated 이벤트를 발행하고 이벤트 핸들러는 이 이벤트를 소비한다. 이벤트 핸들러는 이벤추에이트 트램 사가 프레임워크를 호출하여 CreateOrderSaga를 생성한다.
사가 오케스트레이터 작성: RDBMS 이벤트 저장소 사용 서비스
- 이벤추에이트 로컬은 RDBMS를 사용하므로 이벤추에이트 트램 사가 프레임워크와 동일한 ACID 트랜잭션에 참여할 수 있다.
사가 오케스트레이터 작성: NoSQL 이벤트 저장소 사용 서비스
- NoSQL 이벤트 저장소를 사용하는 서비스는 이벤트 저장소를 업데이트하고 사가 오케스트레이터를 생성하는 액션을 원자적으로 수행하기 어렵다. 그래서 서비스가 애그리거트가 발생시킨 도메인 이벤트에 반응하여 사가 오케스트레이터를 생성하는 이벤트 핸들러를 갖고 있어야 한다.
**6.3.3 이벤트 소싱 기반의 사가 참여자 구현
- 구현하면서 두 가지 이슈를 처리해야 한다.
- 커맨드 메시지를 멱등하게 처리
- 응답 메시지를 원자적으로 전송
커맨드 메시지를 멱등하게 처리
- 멱등한 메시지 처리 메커니즘 활용
- 이벤트의 유일한 속성에서 사가 ID를 추출하거나, 이벤트ID를 사가 ID로 추출한다.
- 처리한 이벤트를 유일성을 보장하면서 식별하고, 처리 이력을 보관하고 있으니, 중복 이벤트 발생시 해당 건을 정확히 인지 할 수 있음.
- 이벤트의 유일한 속성에서 사가 ID를 추출하거나, 이벤트ID를 사가 ID로 추출한다.
응답 메시지를 원자적으로 적송
- 사가 커맨드 핸들러가 애그리거트를 생성/수정할 때, 애그리거트가 발생시킨 진짜 이벤트와 가짜 이벤트 SagaReplyRequested를 모두 이벤트 저장소에 저장한다.
- SagaReplyRequested 이벤트 핸들러는 이벤트에 포함된 데이터로 응답 메시지를 만들어 사가 오케스트레이터 응답 채널에 출력한다.
__6.3.4 사가 오케스트레이터 구현: 이벤트 소싱
구현하면서 세 가지의 설계 이슈를 생각해야 한다.
- 사가 오케스트레이터를 어떻게 저장할 것인가?
- 어떻게 오케스트레이터 상태를 원자적으로 변경하고 커맨드 메시지를 전송할 것인가?
- 어떻게 사가 오케스트레이터가 정확히 한 번만 메시지를 응답하게 만들 것인가?
- 이벤트 소싱으로 사가 오케스트레이터를 저장해보자.
사가는 다음 이벤트를 이용하여 저장할 수 있다.
- SagaOrchestratorCreated: 사가 오케스트레이터가 생성됨.
- SagaOrchestratorUpdated: 사가 오케스트레이터가 수정됨.
커맨드 메시지를 확실하게 전송
- 사가 오케스트레이터가 전송하려는 각 커맨드마다 SagaCommandEvent를 발생 시킨다. 여기에는 목적지 채널, 커맨드 객체 등 커맨드 전송에 필요한 데이터가 모두 담겨있고 이벤트 저장소에 저장된다.
- 이벤트 핸들러는 SagaCommandEvent 처리 후 커맨드 메시지를 목적지 메시지 채널로 보낸다. 이렇게 두 단계로 처리하니 적어도 1회 이상은 커맨드가 전송될 것이고, 동일한 이벤트를 받아 이벤트 핸들러가 호출될 수 있는 구조이기 때문에 SagaCommandEventHandler가 중복된 커맨드 메시지를 전송할 수 있다. 하지만 SagaCommandEvent의 ID를 커맨드 메시지 ID로 사용하면 중복 메시지는 동일한 ID를 가지게 되므로 사가 참여자는 쉽게 중복 메시지를 걸러낼 수 있다.
응답을 꼭 한 번만 처리
- 유일한 메시지 ID를 잘 생성하여ㅏ 중복메시지를 솎아 내면 됨.
6.4 마치며
참고 자료
- https://velog.io/@daehoon12/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%8C%A8%ED%84%B4-6.-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EA%B0%9C%EB%B0%9C-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%86%8C%EC%8B%B1#633-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%86%8C%EC%8B%B1-%EA%B8%B0%EB%B0%98%EC%9D%98-%EC%82%AC%EA%B0%80-%EC%B0%B8%EC%97%AC%EC%9E%90-%EA%B5%AC%ED%98%84
- https://bryceyangs.github.io/various/2023/03/15/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4%ED%8C%A8%ED%84%B4-6%EC%9E%A5/#google_vignette
- https://freeend.tistory.com/112



