# 이벤트 소싱(Event Sourcing) 이란?

데이터 저장 방식의 한 종류이다. 기존에는 App 상에서 특정 로직을 처리한 이후에, DB에는 해당 로직의 결과값만 담는 식으로 진행해 왔다.

그러나, 이벤트 소싱 저장 방식이란 DB가 변경되는 순간의 모든 이벤트를 저장한다.

기존에는 DB 저장 방식은 Book Table에서 A row를 삭제했다가, A row를 다시 생성했을 경우, 

잠시 DB 내에서 삭제되었다가, 생성되었음에도 불구하고, DB 상에서는 현재 시점의 최종 작업 결과가 A row라는 것만 확인할 수 있다.

반면, 이벤트 소싱 방식은 'A row를 삭제' 했을 때의 BookTable 상태와 'A row를 생성' 했을 때의 BookTable 상태를 확인할 수 있다.

매 이벤트마다, BookTable의 상태 전체를 저장한다기 보다는 'A row를 삭제했다'라는 형식의 이벤트 내용만 저장되는 방식이다.

현재 시점의 최종 값을 알기 위해서는 어떻게 할까? 

순차적으로 저장된 이벤트들(일종의 DB Command)을 재생하면 결국 현재 시점의 최종 값을 얻을 수 있게 된다.

(현재의 상태는 변화의 총합으로 표현)

그러나, 이벤트들이 많아질 경우 너무 많이 재생해야 하므로, 성능상의 문제가 발생한다.

그래서, Snapshot 기능이 사용된다. 이벤트 발생 1000개 마다, 스냅샷(BookTable의 상태 전체를 저장)를 저장해두어, 처음부터 이벤트를 재생할 필요없이, 1001개부터 이벤트를 재생하여, 현재 시점의 최종값을 얻는 방식이다.

당연히, 이벤트 소싱에서는 UPDATE 혹은 DELETE의 개념이 존재하지 않는다.

그래서, 쓰기 성능에는 좋다.

조회 성능을 위해는 CQRS와 궁합이 좋고, MSA(Microservices Architecture)에 적용하기 좋다고  한다.

idroot_idevent
11카트 생성함
21상품1 추가함
31상품2 추가함
41상품2 제거함
51배송정보 입력함

# 장점

사용자 요청 쿼리 중에, 현재 DB의 상태값만으로는 확인할 수 없고, 중간에 상태값 변화 정보를 활용해야되는 경우

ex) 카트에 추가되었다가 제거된 상품 목록을 뽑고 싶다고 했을 때 / 별도로 그 특정한 상태를 저장하지 않는다면 어려운 작업


# 구현 : Event의 저장

이벤트 저장에 사용할 수 있는 저장소는 주로 이벤트 저장에 특화된 데이터 저장소인 EventStore database를 사용한다.

- 이벤트 저장에 특화된 데이터 저장소 사용 (e.g. eventstore.org)

- NoSQL or 관계형 데이터베이스 사용

이벤트는 데이터베이스에 직렬화(serialize)해서 저장하고 역직렬화(deserialize)해서 사용한다. 

각 저장하는 방식은 전략에 따라 다른데 관계형 테이터베이스를 사용한다면 이벤트명(주로 이벤트 타입명)과 데이터(주로 payload) 등으로 분리해서 저장한다. 이벤트의 정규 구조가 단순하기 때문에 단순히 위에서 언급한 데이터베이스가 아니더라도 용도에 맞게 선택할 수 있다.

CQRS에서는 무작정 생성해도 충돌을 피할 수 있는 만큼 큰 id(예로 128bit GUID)를 생성해서 사용한다.

스냅샷 기능도 특화된 저장소라면 시스템이 알아서 처리해주지만 그 외에는 이 문제를 고민해서 저장 방식을 설계해야 한다.


# 왜 CQRS와 궁합이 좋은가?

현재 상품1의 재고량을 파악하기 위해 모든 이벤트를 투영하는 대신 재고 테이블에서 상품1의 재고량을 바로 찾아보는 것이 훨씬 쉽다. 

다시 말하면 저장은 이벤트로 하지만 조회는 투영된 데이터, 구체화(materialised)된 데이터를 대상으로 수행하고 싶은 것이다. 

이런 맥락에서 자연스럽게 명령과 조회의 책임을 분리하는 패턴(Command and Query Responsibility Segregation, CQRS)을 적용하는 것이 좋다. 좀 더 나아가서 아예 트랜잭션용 디비와 쿼리용 디비를 따로 준비해서 그들간 싱크는 서비스 브로커라든가 하는 것들을 통해 알아서 하게 하고 쿼리는 읽기 전용 디비에서, 트랜잭션은 쓰기 전용 디비에서 이루어지게 하면 된다


# 리드 모델은 어떻게 최신 데이터를 유지하는가?

새 이벤트는 이벤트 버스(event bus)를 통해 전파되는데 리드 모델에 변화를 투영하는 경우에도 이벤트 리스너로 발행되는 이벤트를 관찰하고 있다가 리드 모델을 갱신해야 하는 이벤트가 발생하는 순간에 갱신할 수 있다


# 추가 설명

애플리케이션 내에서 가능한 모든 액티비티들을 이벤트로 전환해서 별도의 이벤트 스트림 (event stream) 디비에 저장하는 방식이다. 이벤트 스트림 디비는 오로지 추가만 가능하게끔 해서 계속 이벤트들이 쌓이게 만들고 실제로 내가 필요한 데이터를 구체화 (materialised) 시키는 시점에서는 그 때 까지 축적된 데이터를 바탕으로 작성

CartCreatedEvent, ItemAddedEvent, ItemRemovedEvent, ShippingDetailsAddedEvent 등등으로 이벤트를 만들고 그 이벤트들은 오직 자기가 필요한 테이터만 받아 이벤트 스토어 (혹은 이벤트 스트림)에 저장한다. 그리고, 이를 바탕으로 구체화시킨 데이터(materialised view)를 디비에 저장하고, 별도의 리플레이를 통해 현재 시점의 뷰를 쿼리로 보여준다. 바로 이 시점에서 앞서 언급한 CQRS 패턴이 연결되는데, 이벤트 스트림에서 뽑아내서 구체화 시킨 데이터를 디비에 저장시키고 (Command), 특정 시점에 맞춰진 뷰(Query)를 화면에 뿌려주는 식


브라우저에서 어떤 액션이 발생(Input 입력 변경, select 변경)했을 때 그 액션을 이벤트로 만들어서 Web API로 POST 리퀘스트를 보낸다는 것이다. 앞서 언급했다시피 이벤트 소싱 패턴은 이벤트 스트림에 계속 이벤트를 추가하는 방식이기 때문에 여기서는 POST 리퀘스트를 보내는 것이 적절하다


API 콘트롤러에서는 그다지 보여줄 것이 없다. 콘트롤러 안에 보이는 서비스 레이어를 주목하도록 하자. this._service.ChangeSalutationAsync(request) 라는 메소드가 보이는가? 이 메소드가 실제 이벤트를 처리하는 로직


1. 우선 등록된 수많은 REQUEST HANDLER 중에서 해당 리퀘스트를 처리할 수 있는 핸들러를 찾는다.

2. 해당 핸들러가 리퀘스트를 이벤트로 변환시킨다.

3. 이벤트 프로세서의 ProcessEvent() 메소드에 해당 이벤트를 보내서 처리한다.

3-1. 해당 이벤트를 처리할 수 있는 EVENT HANDLER를 모두 찾는다.

3-2. 각각의 이벤트핸들러에 정의되어 있는 ProcessAsync() 메소드를 호출하여 이벤트를 처리한다.

4. 처리된 리퀘스트는 리플레이 시켜서 다시 리스폰스로 변환시켜 반환한다. (브라우저에서 현재 시점의 결과를 확인 가능) 


Submit 버튼을 누르지 않더라도, 이벤트 스트림에만 각 액션이 저장이 된다. 그러나 쿼리용 읽기전용 디비에는 반영이 되어 있지 않는다.




# 출처

https://medium.com/@mjspring/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%86%8C%EC%8B%B1-event-sourcing-%EA%B0%9C%EB%85%90-50029f50f78c

이벤트 소싱 event-sourcing 패턴 정리(https://edykim.com/ko/post/eventsourcing-pattern-cleanup/)

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기