쓰기 최적화 : 이벤트 소싱(Event Sourcing)

Updated:

쓰기 최적화 : 이벤트 소싱(Event Sourcing)

아키텍처 유형 패턴 유형 설명
마이크로서비스 외부 아키텍처 인프라 패턴 마이크로서비스를 지탱하는 하부구조 인프라를 정의하는 패턴들
  플랫폼 패턴 인프라 위에서 마이크로서비스를 운영 관리를 지원하는 플랫폼 차원의 패턴들
  마이크로서비스 통신/관계 패턴 마이크 서비스 유형 간의 관계를 정의하고 처리하는 패턴들
마이크로서비스 내부 아키텍처 마이크로서비스 내부 패턴 마이크로서비스 내부의 외부 연동, 비지니스로직,저장소 처리를 정의하는 패턴들

지금까지 살펴 본 사가 패턴 및 CQRS 패턴이 비즈니스 불일치를 피하기 위해서는 저장소에 저장하는 것과 메시지를 보내는 것이 원자성(atomicity) 을 가져야 한다. 즉 저장소에 저장하는 일과 메시지를 보내는 작업이 언제나 완전하게 진행되어 종료되거나 같이 실행 되지 않아야 된다는 말이다. 그렇지만 객체의 상태변화를 이벤트 메시지로 발행하고 또 객체 상태를 관계형 저장소를 사용하는 경우 SQL 질의어로 변환해서 처리하는 것이 매우 번거롭고 까다롭다 . 또한 CQRS패턴으로 명령 측면과 조회측면으로 분리했더라 하더라도 명령 측면에 추가,변경,삭제 작업이 한꺼번에 발생하기 때문에 동시 업데이트 문제가 발행할 수도 있다. 또 메시지 처리, 명령 처리를 2가지 기능을 수행하므로 빠르지도 않다.

그렇다면 이런 메시지 발행 및 명령 처리의 원자성을 보장하고 및 성능도 최적화하는 방법은 무엇일까? 보통 비지니스 처리시 데이터 처리는 항상 데이터의 최종 상태를 현행화 시키는 방식으로 진행되었다.

예를 들어 쇼핑몰의 장바구니가 있다고 생각해 보자. 장바구니에 몇몇 품목을 추가/삭제 하는 과정을 보자. 만약 일반적인 관계형 DB와 JAVA언어를 사용한다면 , 장바구니,품목 데이터 모델이 정의 되어 있어야 하고 장바구니 상태 변경 시 매번 트랜잭션결과를 반영하여 장바구니 데이터 모델 현행화 해야 한다. 객체의 상태변화 와 데이터 모델의 결과는 다음과 같다.

  1. 사용자 A의 장바구니 객체 생성 → 장바구니 테이블에 사용자 A의 장바구니 로우(row) 생성.
  2. 품목 1 객체 추가 → 품목테이블에 사용자 A장바구니의 품목 1 추가
  3. 품목 2 객체 추가→ 품목테이블에 사용자 A장바구니의 품목 2 추가
  4. 품목 1 객체 제거 → 품목테이블에 사용자 A장바구니의 품목 1 삭제
  5. 품목 2 객체 수량 변경 → 품목테이블에 사용자 A장바구니의 품목 2 정보 수정

이렇게 객체의 상태 변경에 따라 추가/변경/삭제 데이터 처리가 계산되고 수행되어야 한다. 따라서 이런 과정은 복잡해지고 많은 변환 처리로 느릴 수 밖에 없다. 특히 인스턴스가 여러 개로 확장될 때 동시 업데이트 및 교착상태로부터 안전하지 못 할 수 도 있다.

그렇지만 그 이런 트랜잭션 자체를 저장하면 어떨까? 이벤트 소싱(event Sourcing) 이라고 하는 기법인데 이벤트 소싱에서는 상태가 아닌 트랜잭션 차체를 저장 하자는 전략이다. 상태 변경 이벤트를 바로 이벤트 저장소에 저장한다. 메시지 브로커와 데이터 저장소를 분리하지 않고 하나로 사용하면 된다. 복잡한 과정이 없으니 쓰기 속도가 훨씬 빠르다. 그럼 현재 시점의 상태가 필요할 때는 어떻게 해야 하나? 상태가 필요하면 상태의 출발점부터 모든 기록된 상태 변경 트랜잭션을 순차적으로 계산한다.

처음부터 모든 트랜잭션을 처리하는 것이 부담된다면 매일 자정에 상태를 계산한 후 스냅샷으로 저장한 후 현 상태 정보가 필요해지면 스냅 샵(Snapshot) 이후의 트랜잭션만 처리하면 된다. 이런 방식은 특정 시점의 상태가 필요하면 재현할 수도 있기 때문에 별도의 트랜잭션 성HISTORY 로그 데이터를 쌓을 필요도 없다.

또 중요한 점은 명령 측면과 조회 측면의 서비스가 CRUD를 모두 처리할 필요없이 CR만 처리하면 된다는 것이다. 저장소에서 변경과 삭제가 발생하지 않기 때문에 명령 측면 서비스를 여러 개 확장해도 동시 업데이트 및 교착상태가 발생하지 않는다.

아래 그림은 이러한 이벤트 저장소의 데이터 형태이다. 이벤트 아이디가 있고, 어떠한 상태인지, 어떠한 객체의 이벤트인지 그리고 그 변경 내용이 JSON 형태로 그대로 들어간다. JSON은 오브젝트 형태 라 상태 객체가 그대로 들어간 것과 같다.

이벤트 스트림 저장

CRUD가 모두 발생하지 않고 CR만 사용하는 개념이라 쓰기서비스를 여러 개 확장해도 동시 업데이트 및 교착상태, 정합성 등의 문제가 발생되지 않는다. 또 저장과 동시에 그대로 메시지 형태로 메시지 브로커에 발행하여 다른 서비스에 영향을 줄 수 도 있다.

다시 정리하면 아래 그림처럼 이벤트 소싱은 모든 트랜잭션의 상태를 바로바로 계산하지 않고 별도의 이벤트 스트림으로 이벤트 스트림 저장소에 저장하는 방식이다. 이벤트 스트림 저장소는 오로지 추가만 가능하게끔 해서 계속 이벤트들이 쌓이게 만들고 실제로 내가 필요한 데이터를 구체화시키는 시점에서는 그때까지 축적된 트랜잭션을 바탕으로 그 상태를 계산하여 구성한다.

이벤트 저장소는 이벤트 데이터베이스 역할 뿐만 아니라 메시지 브로커처럼 작동한다. 이는 데이터 저장 처리 메커니즘과 메시지 큐 같은 이벤트를 전달하기 위한 메커니즘을 통합하여 복잡함을 감소시키고 특히 쓰기 성능을 최적화 한다. 또한 100%로의 정확한 감사 로깅을 제공하고 객체의 예전 상태를 재구성하는 것이 간단해 지며 외부 어플리케이션도 이벤트 전달도 간편하다.

이벤트 소싱 개념도