Event Sourcing과 CQRS

semin·2024년 4월 21일

기존 데이터 저장 방식

기존에는 현실의 정보를 최종 상태만 데이터 베이스에 저장하는 방식으로 구현하였다. 이 방식은 직관적으로 현재 상태를 표현하기 때문에 개발자나 사용자가 보기에는 큰 문제가 없다. 하지만, 비즈니스 관점에서는 다를 수 있다.

스포티파이 구독 서비스를 예시로 사용자에 대한 구독 정보를 사용자 id, 구독 요금제, 구독 만료일 형태로 기록하고 있다고 가정해보자.

현재 구독을 했는지, 어떤 요금제를 선택했는지 파악은 가능하다. 비즈니스 관점에서는 이 사용자가 언제 해당 요금제를 구독하기 시작했는지, 어떤 사용자가 어느 시기에 구독을 취소하거나 요금제를 변경했는지 알 수 없어 답답하다.

물론 구독 내역을 알고싶다는 요구사항이 있다면 일대다 매핑으로 사용자의 구독 내역을 저장할 수는 있다. 문제는 비즈니스가 확장될 때마다 History 테이블을 만들게 되면 스키마는 점점 복잡해질 수 밖에 없다.

정리

  • 장점 : 현재 도메인 객체의 상태를 직관적으로 표현할 수 있다.
  • 단점 : 도메인 객체의 히스토리를 알 수 없다.

Event Sourcing

이벤트 소싱은 이름 때문에 Event Driven과 연관지어서 생각할 수 있지만 본질적으로는 데이터를 저장하는 기법 중 하나일 뿐이다. 이벤트 소싱에서는 도메인의 상태 변화를 이벤트로 기록한다. 아래 예시 이미지를 통해 간단하게 이해해보자.
이벤트는 수정, 삭제는 일어나지 않고 오직 삽입만 일어난다.

계좌가 개설되었을 때 초기 잔고가 0원이라고 정의되었다면, 위 1번부터 3번 이벤트가 차례대로 저장되어 EventHandler를 통해 이벤트를 재생하면 오른쪽의 현재 계좌 상태를 획득할 수 있게된다.

위와 같이 도메인에 발생하는 모든 이벤트를 기록하기 때문에 별도의 수정이나 삭제가 없어도 현재 상태를 변경할 수 있다. 수정이나 삭제가 없고 오로지 이벤트 추가만 하기 때문에 동시성에서도 자유롭다는 장점이 있다.

각각의 이벤트는 다음과 같은 key&value 형태로 저장된다.

  • Key
    • Object Id : 어떤 도메인 객체에 대한 이벤트인지 표현한다. 예시 이미지에서 "2번 계좌" 가 해당 속성에 해당한다.
    • Version : 이벤트 버전을 의미한다. 예시 이미지에서의 넘버링이 버전에 속한다.
  • Value
    • Event Type : 이벤트 종류를 설명하며 과거형으로 작성한다. 예시에서 "계좌에 입금했다" 와 같은 부분이 이벤트 타입을 의미한다.
    • Serialized Payload : 이벤트에 대한 내용을 직렬화하여 저장한다. 예를들면, 10000원 입금은 {"balance" : 10000} 과 같이 표현될 수 있다. 꼭 JSON 형태로 직렬화 할 필요는 없다.

의문 - 이벤트 버전을 기록하거나 이벤트의 유효성 검증을 위해서는 동시성 이슈 고려가 필요한 것이 아닐까? 그렇지 않으면 같은 버전의 이벤트가 여러 개 생성된다거나, 유효하지 않은 이벤트가 저장될 수 있을 것 같다는 생각이 들었다.

스냅샷

객체의 상태를 확인하기 위해 이벤트를 재생해야 하는데, 이벤트가 많이 축적되었다면 각각의 객체 상태를 확인하기 위한 비용이 점점 많이 들어갈 수 밖에 없다. 이를 해결하기 위해 스냅샷이라는 개념이 존재한다. 스냅샷은 이벤트가 생기는 특정 시점마다 생성하여 모든 이벤트를 재생하지 않고도 빠르게 객체의 현 상태를 재현할 수 있도록 한다.

스냅샷을 사용하지 않을 때 객체의 현재 상태는 다음과 같이 표현할 수 있다.

 Staten=Seed+i=1nEventi\displaystyle\ State_n \displaystyle \displaystyle= Seed + \sum_{i=1}^{n}Event_i

만약, n-1 번째에서 스냅샷을 찍어놨다면 객체의 상태는 다음과 같이 표현할 수 있다.

 Staten=Staten1+Eventn\displaystyle\ State_n \displaystyle \displaystyle= State_{n-1} + Event_n

축적된 이벤트가 많을수록, 스냅샷의 이점을 크게 볼 수 있다.

문제점

스냅샷을 이용하더라도 기존 데이터베이스에 상태를 저장하는 방식보다 데이터를 읽을 때의 성능은 떨어질 수 밖에 없다. "잔고가 50,000원 이상인 계좌를 모두 조회해줘" 라는 요구사항이 있다면 스냅샷을 통해 각각의 객체를 로딩하고, 조건에 맞는지 확인 한 뒤 필터링하는 작업까지 거쳐야 한다.

이러한 문제를 보완하기 위해 이벤트 소싱은 독립적으로 사용되지 않고 CQRS 패턴과 함께 구현되는 것이다.

이벤트 소싱 개념 정리
이벤트 소싱이란, 데이터를 저장하는 기법으로 객체의 모든 상태 변화를 이벤트로 기록하여 히스토리 정보를 표현할 수 있다.

  • 장점 - 객체에 일어나는 모든 이벤트를 기록하여 히스토리를 확인할 수 있으며 동시성 이슈로부터 자유롭다.
  • 단점 - 현재 상태에 대한 읽기 성능이 기존 데이터 저장 방식보다 매우 비효율적이다.

CQRS

CQRS는 Command And Query Responsibility Segrigation의 약자로 명령 조회 분리를 나타낸다. 명령이란 시스템의 상태를 변경시키는 것을 의미하며 조회는 시스템의 상태를 조회하는 것을 의미한다. 이러한 두 개의 동작을 분리하는 것이 CQRS의 정의이다.

CQRS는 MSA, 이벤트소싱 등 다양하게 조합되어 사용되어 복잡해보이지만, 이러한 아키텍처나 환경이 CQRS와 잘 들어맞아서 자주 조합되어 사용될 뿐, CQRS 자체가 복잡한 것은 아니다.

그렇다면 왜 CQRS는 MSA와 같이 언급되는 경우가 많을까? MSA가 적용될 정도라면 이미 서비스가 방대하고 많은 트래픽을 다루며 복잡한 도메인을 다루고 있을 가능성이 크다. 이런 배경에서는 쓰기 모델과 읽기 모델이 얽혀서 소스코드의 품질과 서비스의 성능이 저하될 가능성이 더욱 높을 것이다. 또한, 기본적으로 MSA 기반이라면 분산 서버를 위한 인프라가 구성되어 있을 것이므로 CQRS를 적용해야하는 필요성과 적용할 수 있는 환경이 갖춰져 있어 이들이 함께 구현되는 경우가 많은 것이라고 추측된다.

구현 방법

앞서 말했듯 CQRS는 간단하게 구현될 수도 있고 다양한 기술과 함께 구현될 수도 있다. 크게 구분되는 세가지 구현 방법에 대해 알아보자.

  1. 단일 DB 사용, 조회와 명령 모델 분리
  • 장점 : 복잡한 인프라 없이 간단하게 CQRS를 적용할 수 있다.
  • 단점 : DB 수준에서의 성능상 이점이 없다.
  1. DB를 분리하여 명령과 조회 모델에 각각의 DB 사용
  • 장점 : DB 수준에서의 성능 이슈를 개선할 수 있으며, 쓰기 수준에서의 데이터 무결성을 보장할 수 있다.

  • 단점 : 분리된 DB 사이의 데이터 동기화 신뢰성을 확보해야 한다.

    일반적으로 명령에는 RDBMS를 사용하여 강력한 정규화와 Transaction의 지원으로 데이터의 무결성을 확보한다. 조회에는 NoSQL을 사용하여 조회 요구사항에 특화된 인덱싱 전략과 비정규화를 적용한다.

  1. EventSourcing 적용
  • 장점 : 히스토리 기록, 동시성 이슈로부터 자유로움 등 이벤트 소싱의 장점을 활용하며 읽기 모델을 분리하여 읽기 성능 이슈도 해결할 수 있다.
  • 단점 : 시스템의 복잡성을 증가시킬 수 있으며 러닝커브가 상승한다.

참고자료

https://gyuwon.github.io/blog/2020/06/23/essence-of-event-sourcing.html
https://www.youtube.com/watch?v=TDhknOIYvw4
https://www.youtube.com/watch?v=12EGxMB8SR8
https://justhackem.wordpress.com/2016/09/17/what-is-cqrs/
https://azderica.github.io/02-architecture-msa/

profile
블로그 이전 -> https://choicco.tistory.com/

0개의 댓글