AI와 함께 정리하기.
개념 설명
- 이벤트 소싱 (Event Sourcing): 시스템의 **'현재 상태(Current State)'**를 저장하는 대신, 그 상태에 도달하기까지 발생한 모든 '이벤트(Event)'의 순차 목록을 저장하는 방식입니다.
- 예시: 은행 계좌의 '현재 잔액'(9,000원)을 저장하는 것이 아니라, 입금(+10,000원), 출금(-1,000원) 이라는 이벤트들을 순서대로 모두 저장합니다. 잔액은 이 이벤트들을 처음부터 재생(replay)하면 언제든지 계산할 수 있습니다.
- CQRS: 시스템의 명령(Command, 데이터 변경) 책임과 조회(Query, 데이터 읽기) 책임을 완전히 분리하는 패턴입니다.
- Command: 데이터의 상태를 변경하는 모든 작업 (예: CreateOrderCommand, UpdateStockCommand). 이 Command의 결과로 이벤트가 발생합니다.
- Query: 데이터를 읽는 모든 작업. 사용자가 보는 '뷰(View)'를 처리합니다.
작동 방식
이 두 패턴을 조합하면 다음과 같은 아키텍처가 만들어집니다.
- Command 처리 (데이터 변경):
- 사용자가 '주문 생성' 요청을 보냅니다 (PlaceOrderCommand).
- 시스템은 이 Command를 처리하고, 그 결과로 OrderPlaced 라는 이벤트가 생성됩니다.
- 이 OrderPlaced 이벤트는 **이벤트 스토어(Event Store)**라는 특수한 DB에 영구적으로 저장됩니다. (이것이 "events를 데이터 쌓고"에 해당합니다.) 이벤트는 수정되거나 삭제되지 않고 계속 쌓이기만 합니다.
- View(Read Model) 생성:
- 별도의 프로젝터(Projector) 또는 **이벤트 핸들러(Event Handler)**가 이벤트 스토어를 계속 감시합니다.
- 새로운 OrderPlaced 이벤트가 감지되면, 프로젝터는 이 정보를 바탕으로 **조회용 데이터베이스(Read Database)**를 업데이트합니다.
- 예를 들어, 주문 목록 뷰 테이블에 새로운 주문 정보를 추가하고, 월별 매출 통계 뷰 테이블의 값을 업데이트합니다. (이것이 "이벤트들의 최종 모습을 view로 처리"에 해당합니다.)
- Query 처리 (데이터 조회):
- 사용자가 '내 주문 목록 보기'를 요청합니다.
- 이 요청은 복잡한 이벤트 스토어를 거치지 않고, 이미 조회를 위해 최적화된 주문 목록 뷰 테이블을 직접 읽어서 매우 빠르게 결과를 반환합니다.
이 방식의 장점
- 완벽한 감사 추적: 데이터가 어떻게 변경되었는지 모든 이력이 이벤트로 남아있어 문제 추적과 분석에 매우 강력합니다.
- 다양한 뷰 제공: 하나의 이벤트 스트림을 가지고 '최신 상품 목록', '월별 통계', '사용자 활동 로그' 등 다양한 목적의 조회 모델(View)을 유연하게 만들 수 있습니다.
- 성능 및 확장성: 데이터 변경(Write)과 조회(Read)의 부하가 분리되므로, 각 부분의 요구사항에 맞춰 독립적으로 최적화하고 확장할 수 있습니다. 예를 들어, 읽기 작업이 많아지면 조회용 DB만 확장하면 됩니다.
그럼 조심해야 할 것은!?
1. 최종적 일관성 (Eventual Consistency) 문제
가장 먼저 부딪히는 현실적인 문제입니다. 데이터 변경(Command)과 조회(Query) 모델이 분리되어 있어, 데이터를 변경한 직후 바로 조회를 하면 아직 반영되지 않은 이전 데이터가 보일 수 있습니다.
- 문제 상황:
- 사용자가 게시글을 작성하고 '저장' 버튼을 누릅니다. (PostCreated 이벤트 발생)
- 시스템은 즉시 '게시글 목록' 페이지로 이동시킵니다.
- 하지만 프로젝터(Projector)가 아직 PostCreated 이벤트를 처리하여 '게시글 목록 뷰'를 업데이트하지 못했다면, 방금 작성한 글이 목록에 보이지 않습니다. 사용자는 "저장이 안 됐나?" 하고 혼란에 빠질 수 있습니다.
- 대응 방안:
- UI/UX로 보완: 요청을 보낸 클라이언트에게는 "임시로" 반영된 것처럼 보여주는 '가짜(optimistic) UI'를 적용할 수 있습니다. 서버의 최종 응답을 기다리지 않고, 성공할 것이라 가정하고 화면을 미리 업데이트하는 방식입니다.
- Polling 또는 Websockets: 클라이언트가 뷰 모델이 업데이트될 때까지 짧은 간격으로 재조회(Polling)하거나, 서버가 업데이트 완료 시점에 클라이언트에 푸시 알림(Websocket)을 보내주는 방법을 사용할 수 있습니다.
- Command 후 Query 강제 동기화: 꼭 필요한 경우, Command를 처리한 쪽에서 특정 Query 모델의 업데이트까지 기다렸다가 응답을 주는 방법도 있지만, 시스템의 장점인 '분리'를 해치므로 신중하게 사용해야 합니다.
2. 이벤트 스키마 버전 관리 (Event Schema Versioning)
한번 저장된 이벤트는 **불변(Immutable)**입니다. 하지만 비즈니스는 계속 변하고, 이에 따라 이벤트에 담기는 내용(스키마)도 변경이 필요해집니다.
- 문제 상황:
- 초기에 UserRegistered 이벤트를 (userId, name) 으로 정의했습니다.
- 시간이 지나 email 필드가 필수로 추가되어, 이제 (userId, name, email) 형태의 정보가 필요해졌습니다.
- 시스템을 재시작하거나 뷰를 재구축할 때, 과거의 (userId, name) 이벤트와 현재의 (userId, name, email) 이벤트를 모두 처리할 수 있어야 합니다.
- 대응 방안 (전략적 선택 필요):
- 업캐스팅 (Upcasting): 오래된 버전의 이벤트를 읽어올 때, 실시간으로 최신 버전의 이벤트 구조로 변환하여 처리하는 로직을 추가합니다. 가장 일반적인 방법입니다.
- 여러 버전의 이벤트 핸들러: UserRegistered_V1 핸들러, UserRegistered_V2 핸들러를 모두 구현하여 각각의 버전을 처리하도록 합니다.
- 새로운 이벤트 타입 생성: 필드 추가가 아닌 근본적인 의미 변화가 있을 경우, 아예 UserRegisteredWithEmail 같은 새로운 이벤트를 정의할 수도 있습니다.
이는 초기에 정책을 잘 수립하지 않으면 나중에 기술 부채가 되어 시스템 전체를 흔들 수 있는 매우 중요한 문제입니다.
3. 뷰(Read Model) 재구축의 비용과 시간
뷰 모델에 버그가 있었거나, 새로운 분석을 위한 뷰가 필요할 때 모든 이벤트를 처음부터 다시 재생(Replay)하여 뷰를 재구축(Rebuild)해야 합니다.
- 문제 상황:
- 수억 개의 이벤트가 쌓여있는 시스템에서 '월별 매출 통계' 뷰를 처음부터 재구축해야 합니다.
- 모든 이벤트를 읽고 계산하여 뷰를 만드는 데 수 시간이 걸릴 수 있으며, 그동안 시스템에 상당한 부하를 줍니다.
- 대응 방안:
- 스냅샷 (Snapshot): 전체 이벤트를 재생하는 대신, 특정 주기(예: 1000번째 이벤트마다)로 당시의 최종 상태를 '스냅샷'으로 저장해 둡니다. 재구축 시 가장 최신 스냅샷부터 그 이후의 이벤트만 재생하면 되므로 시간을 크게 단축할 수 있습니다.
- 분산 처리: 이벤트 리플레이 과정을 여러 서버에 분산하여 병렬로 처리함으로써 시간을 단축할 수 있습니다.
4. 복잡성과 가파른 학습 곡선
단순한 CRUD 방식에 비해 이벤트, 커맨드, 애그리거트(Aggregate), 프로젝터 등 생소한 개념이 많고 전체적인 데이터 흐름이 복잡합니다.
- 문제 상황:
- 팀원들이 이벤트 소싱의 개념, 특히 최종적 일관성을 이해하고 코드를 작성하는 데 어려움을 겪습니다.
- 간단한 기능 하나를 추가하더라도 커맨드, 이벤트, 핸들러, 뷰 업데이트 등 여러 파일을 수정해야 하므로 개발 생산성이 초기에 떨어질 수 있습니다.
- 디버깅이 어렵습니다. 데이터의 현재 상태만 봐서는 원인을 알기 어렵고, 어떤 이벤트들이 발생했는지 그 흐름을 추적해야 합니다.
- 대응 방안:
- 충분한 학습과 공감대 형성: 팀 전체가 패턴의 장단점을 명확히 이해하고, 왜 이 방식을 선택했는지 공감대를 형성하는 과정이 필수적입니다.
- 도메인 전문가와의 협력: 비즈니스 '이벤트'를 정확하게 식별하고 정의하기 위해 도메인 전문가와 긴밀하게 협력해야 합니다.
- 점진적 도입: 시스템 전체를 한 번에 바꾸기보다는, 감사 추적이 매우 중요하거나 복잡한 비즈니스 로직을 가진 특정 도메인(예: 주문, 결제)에 먼저 부분적으로 적용해 보는 것이 좋습니다.
5. 인프라 및 도구 의존성
이벤트 소싱과 CQRS는 안정적인 메시지 큐(예: Kafka, RabbitMQ)와 이벤트 저장을 위한 이벤트 스토어(예: EventStoreDB 또는 일반 DB를 커스터마이징) 등 추가적인 인프라를 필요로 합니다. 이는 운영 및 관리의 복잡성을 증가시킵니다.
결론적으로 이벤트 소싱 + CQRS는 **"모든 문제에 대한 만병통치약이 아니다"**라는 점을 기억하는 것이 중요합니다. 데이터의 변경 이력을 모두 추적해야 하는 강력한 요구사항이 있거나, 매우 복잡한 비즈니스 규칙을 가진 도메인에 선택적으로 적용했을 때 그 진가를 발휘하는 고급 아키텍처 패턴입니다.