이벤트 소싱
상태를 저장하지 않고 누적된 데이터를 상태로 삼는 방법
1. CRUD와의 비교
이벤트 소싱을 활용해 확장 가능한 사내 플랫폼 만들기 | 당근 SERVER 밋업 2회 - YouTube 05:43 ~
CRUD는…
- Create
- 객체를 레코드로 추가함
- Read
- 객체를 불러옴
- Update
- 객체를 불러와서 수정 후 저장함
- Delete
- 객체를 삭제함 (또는 삭제했다고 침; soft-delete)
이벤트 소싱은 단 두 가지 밖에 없음
- 이벤트 객체를 레코드로 추가함
- 이벤트 객체 목록을 가져와 순회하며 연산함
익숙한 CRUD는 아래처럼 구현함
- Create
- (생성) 이벤트 객체를 레코드로 추가함
- Read
- 이벤트 객체 목록을 가져와 순회하며 연산함
- Update
- (갱신) 이벤트 객체를 레코드로 추가함
- Delete
- (삭제) 이벤트 객체를 레코드로 추가함
2. 구현
영속적 레이어에는 두 가지만 있으면 된다. 이벤트 목록과 스냅샷.
{NDC21-프로그래밍} 〈쿠키런: 킹덤〉 서버 아키텍처 뜯어먹기! - YouTube
아래는 쿠키런: 킹덤에서 사용하는 사례:
CREATE TABLE journals ( │ -- 이벤트가 구분되는 개체의 ID │ aggregate_id VARCHAR(255) NOT NULL, │ -- 이벤트 번호 (1부터 시작) │ sequence_number INT8 NOT NULL, │ -- 이벤트 타입 (어떤 객체로 역직렬화할지 결정) │ manifest VARCHAR(255) NOT NULL, │ -- protobuf 이벤트 바이너리 │ journal BYTEA NOT NULL, │ -- 이벤트가 만들어진 시간 │ created_at TIMESTAMP NOT NULL, │ │ PRIMARY KEY (aggregate_id, sequence_number) );
CREATE TABLE snapshots ( │ -- 이벤트가 구분되는 개체의 ID │ aggregate_id VARCHAR(255) NOT NULL, │ -- 스냅샷이 반영하고 있는 마지막 이벤트 번호 │ -- 예시) 100일 경우 100번 째 이벤트까지 리플레이한 상태 │ sequence_number INT8 NOT NULL, │ -- 이벤트 타입 (어떤 객체로 역직렬화할지 결정) │ manifest VARCHAR(255) NOT NULL, │ -- protobuf 이벤트 바이너리 │ snapshot BYTEA NOT NULL, │ -- 이벤트가 만들어진 시간 │ created_at TIMESTAMP NOT NULL, │ │ PRIMARY KEY (aggregate_id, sequence_number) );
- 목표 상태에 대한 Entity를 정의
- Event Repository를 정의
- 각각의 Event를 정의
- 각각의 Event에 대한 EventHandler 정의
3. 장단점
3.1. 장점
- DB 사용이 단순해진다
- 이벤트, 스냅샷 테이블만 있으면 된다
- 콘텐츠 개발 시 DB 스키마를 수정할 필요가 없어진다
- INSERT, SELECT 쿼리만 사용한다
- 쿼리, 인덱스 등의 DB 성능 문제가 발생할 문제가 없다
- DB 사용 패턴이 일정하므로 부하 예측도 쉽다
- 상태 변화에 대해 immutable event로 저장되어 audit log로서의 기능을 수행할 수 있다
- 상태 변화가 순서대로 저장되어 롤백이 쉽다
- 각각의 Event 단위로 이슈 발생을 추적할 수 있다
- 비동기 전환이 간단하다
3.2. 단점
- DDD Aggregate로 모델링이 힘든 경우 이벤트 소싱도 어렵다
- DB에서 직접 상태를 조회 & 변경하기 어렵다
- 별도 툴 개발이 필수적이다
- 로깅 및 데이터 분석으로 활용하기는 기대보다 어렵다1
4. 참고
Footnotes:
1
[NDC21-프로그래밍] 〈쿠키런: 킹덤〉 서버 아키텍처 뜯어먹기! 21:52