2026년 3월 11일 수요일

이벤트 소싱과 CQRS 패턴: 데이터 일관성을 보장하는 방법



"왜 이메일이 안 갔을까요?"

프로젝트 관리 시스템에서 작업을 완료했는데, 관련자에게 알림 이메일이 가지 않았습니다.
데이터베이스를 확인해보니 작업 상태는 "완료"로 되어 있지만, 이메일 발송 기록은 없습니다.

"서버가 다운됐다가 다시 올라왔는데, 그 사이에 뭔가 꼬인 것 같아요."

이런 문제는 데이터 불일치 때문입니다.
여러 서비스가 같은 데이터를 공유하면서 발생하는 전형적인 문제죠.

User Service가 Task를 "완료" 상태로 업데이트하고, Email Service가 이메일 발송을 시도하는 중에 서버가 다운되면, Task는 완료되었지만 이메일은 미발송되어 데이터 불일치가 발생합니다.

하지만 이벤트 소싱(Event Sourcing)과 CQRS(Command Query Responsibility Segregation) 패턴은 이 문제를 해결합니다.

오늘은 이 두 패턴으로 데이터 일관성을 보장하는 방법을 알아봅니다.

전통적 데이터 모델의 문제

전통적 방식:

  • 현재 상태만 저장
  • 여러 서비스가 같은 DB 공유
  • 트랜잭션으로 일관성 유지

문제점:

  • 서버 다운 시 데이터 손실
  • 서비스 간 불일치
  • 변경 이력 손실
  • 복잡한 동기화

실제 예시:

  1. User Service: Task "완료" 상태 업데이트
  2. Email Service: 이메일 발송 시도
  3. 서버 다운
  4. 결과: Task는 완료되었지만 이메일은 미발송
  5. 데이터 불일치 발생

이벤트 소싱의 해결책

이벤트 소싱은 현재 상태가 아닌 이벤트 스트림을 저장합니다.

핵심 개념:

  • 모든 변경사항을 이벤트로 기록
  • 이벤트를 재생하여 현재 상태 재구성
  • 이벤트가 "진실의 원천"

전통적 방식 vs 이벤트 소싱:

전통적 방식:

# 현재 상태만 저장
task.status = "완료"
db.save(task)

이벤트 소싱:

# 이벤트만 저장
event = TaskCompletedEvent(task_id, timestamp, user_id)
event_store.append(event)

장점:

  • 모든 변경 이력 보존
  • 시간 여행 가능
  • 서버 다운 시에도 복구 가능
  • 이벤트 재생으로 일관성 보장

CQRS: 읽기와 쓰기 분리

CQRS는 읽기와 쓰기를 완전히 분리합니다.

Command Side (쓰기):

  • 상태 변경만 담당
  • 이벤트 생성 및 저장
  • 최적화: 빠른 쓰기

Query Side (읽기):

  • 읽기 전용 뷰
  • 사전 계산된 데이터
  • 최적화: 빠른 읽기

장점:

  • 읽기/쓰기 독립적 확장
  • 각각 최적화 가능
  • 복잡한 쿼리 가능

실제 구현 예시

예시 1: 작업 상태 변경

전통적 방식:

# 여러 서비스가 같은 DB 업데이트
task.status = "완료"
db.save(task)

# 이메일 서비스가 별도로 확인
if task.status == "완료":
    send_email()

문제: 서버 다운 시 이메일 미발송

이벤트 소싱 방식:

# 이벤트만 저장
event = TaskCompletedEvent(task_id, timestamp, user_id)
event_store.append(event)

# 이벤트 발행 (비동기)
event_bus.publish(event)

# 이메일 서비스가 이벤트 수신
@event_handler(TaskCompletedEvent)
def send_completion_email(event):
    send_email(event.task_id)

결과: 서버가 다운되어도 이벤트는 저장되고, 복구 후 재처리 가능

💡 Plexo의 AI Task Breakdown 기능은 AI가 생성한 WBS(작업·시간·우선순위)도 이벤트로 기록합니다. AI 계획 생성 → 팀원 수정 → 최종 확정까지 모든 과정이 이벤트 스트림에 보존되어, 계획 변경의 전체 이력을 투명하게 추적할 수 있습니다.

예시 2: 대시보드 조회

전통적 방식:

# 복잡한 쿼리로 집계
dashboard = db.query("""
    SELECT 
        COUNT(*) as total_tasks,
        SUM(CASE WHEN status='완료' THEN 1 ELSE 0 END) as completed,
        ...
    FROM tasks
    WHERE project_id = ?
""", project_id)

문제: 느린 쿼리, DB 부하

CQRS 방식:

# Command Side: 이벤트 저장
event = TaskCreatedEvent(...)
event_store.append(event)

# Query Side: 사전 계산된 뷰
dashboard = read_db.query("""
    SELECT * FROM project_dashboard_view 
    WHERE project_id = ?
""", project_id)

결과: 빠른 조회, DB 부하 감소

이벤트 소싱 구현 가이드

Step 1: 이벤트 정의 (1주)

이벤트 구조:

  • 이벤트 ID: 고유 식별자
  • 이벤트 타입: TaskCreated, TaskCompleted 등
  • 타임스탬프: 발생 시간
  • 페이로드: 변경 내용
  • 메타데이터: 사용자, IP 등

예시:

class TaskCompletedEvent:
    event_id: str
    task_id: str
    timestamp: datetime
    user_id: str
    completion_time: int  # 소요 시간

Step 2: 이벤트 저장소 구축 (2주)

요구사항:

  • 영구 보관
  • 빠른 쓰기
  • 순서 보장
  • 확장 가능

도구 선택:

  • 소규모: PostgreSQL
  • 중규모: EventStore
  • 대규모: Kafka + EventStore

Step 3: 이벤트 핸들러 구현 (2주)

핸들러 유형:

  • 이메일 발송
  • 알림 전송
  • 대시보드 업데이트
  • 리포트 생성

예시:

@event_handler(TaskCompletedEvent)
def update_dashboard(event):
    dashboard = read_db.get_dashboard(event.project_id)
    dashboard.completed_tasks += 1
    read_db.save_dashboard(dashboard)

Step 4: 읽기 모델 구축 (2주)

읽기 모델:

  • 대시보드 뷰
  • 리포트 뷰
  • 검색 인덱스

업데이트:

  • 이벤트 발생 시 자동 업데이트
  • 배치 처리로 최적화

CQRS 구현 가이드

Step 1: Command/Query 분리 (1주)

Command:

  • 상태 변경만
  • 이벤트 생성
  • 쓰기 DB에 저장

Query:

  • 읽기만
  • 읽기 DB에서 조회
  • 캐시 활용

Step 2: 읽기 모델 최적화 (2주)

최적화 전략:

  • 자주 조회하는 데이터 사전 계산
  • 인덱스 최적화
  • 캐시 활용

Step 3: 동기화 메커니즘 (2주)

동기화:

  • 이벤트 발생 시 읽기 모델 업데이트
  • 배치 처리로 성능 최적화
  • 최종 일관성 보장

이벤트 소싱의 장단점

장점

  1. 완전한 감사 추적

    • 모든 변경사항 기록
    • 규정 준수 용이
  2. 시간 여행

    • 과거 상태로 되돌리기
    • 특정 시점 상태 확인
  3. 유연한 확장

    • 새로운 읽기 모델 추가 용이
    • 분석 및 리포팅 강화
  4. 일관성 보장

    • 이벤트 재생으로 일관성 유지
    • 서버 다운 시에도 복구 가능

단점

  1. 복잡도 증가

    • 구현이 복잡함
    • 학습 곡선 존재
  2. 저장 공간

    • 이벤트가 계속 쌓임
    • 장기 보관 시 용량 증가
  3. 성능 고려

    • 많은 이벤트 재생 시 느려질 수 있음
    • 스냅샷 전략 필요

실전 체크리스트

이벤트 소싱 도입 전:

  •  이벤트 스키마 정의
  •  이벤트 저장소 선택
  •  읽기 모델 설계
  •  이벤트 핸들러 구현
  •  동기화 메커니즘 구축
  •  스냅샷 전략 수립

핵심 정리

이벤트 소싱과 CQRS는 복잡하지만 강력한 패턴입니다.

핵심 가치:

  • 모든 변경사항 기록
  • 시간 여행 가능
  • 읽기/쓰기 최적화
  • 일관성 보장

이 패턴을 적용하면, 데이터 일관성 문제를 근본적으로 해결할 수 있습니다.

오늘부터 시작하세요.
작은 변화가 큰 차이를 만듭니다.


AI 기반 프로젝트 계획과 이벤트 소싱을 지원하는 프로젝트 관리 도구가 필요하신가요? Plexo를 확인해보세요.

댓글 없음:

댓글 쓰기