c#

C# CQRS & Event Sourcing 구현 사례 가이드

개발에대해 2025. 9. 29. 13:17
반응형
C# CQRS & Event Sourcing 구현 사례 가이드

C# CQRS & Event Sourcing 구현 사례 가이드

1. CQRS란 무엇인가?

안녕하세요! 오늘은 C#과 ASP.NET Core를 활용한 CQRS(Command Query Responsibility Segregation)Event Sourcing 구현 사례를 소개하려고 합니다. CQRS는 이름 그대로 명령(Command)과 조회(Query)를 분리하는 아키텍처 패턴입니다. 즉, 데이터 쓰기와 읽기를 분리하여 각각 최적화할 수 있어 복잡한 시스템에서도 성능과 유지보수성을 높이는 전략입니다.

일반 CRUD에서는 한 모델로 읽기와 쓰기를 처리하지만, CQRS에서는 명령과 조회 모델을 분리하고, 필요하다면 이벤트(Event)를 통해 상태 변화를 추적합니다.

2. Event Sourcing 개념

Event Sourcing은 시스템 상태를 이벤트 로그(Event Log) 형태로 저장하는 방식입니다. 즉, 현재 상태를 직접 저장하는 것이 아니라, 상태 변경 이벤트를 기록하고 필요할 때 이를 재생(Replay)하여 상태를 복원합니다.

장점:

  • 모든 상태 변경 이력 보존 → 감사(Audit) 가능
  • 시간 여행(Time Travel) 기능 → 과거 상태 재현 가능
  • 분산 시스템에서 이벤트 기반 아키텍처와 자연스럽게 연결

단점:

  • 이벤트 설계와 버전 관리가 까다롭다
  • 초기 학습 곡선이 존재
  • 조회 성능을 위해 Read 모델 추가 필요

3. CQRS + Event Sourcing 구조

일반적인 구조는 다음과 같습니다.

Client
  │
  ▼
Command Handler ---> Event Store ---> Event Publisher
  │                                 │
  ▼                                 ▼
Write Model                        Read Model

즉, 클라이언트 요청 → Command → 이벤트 저장 → 이벤트 발행 → Read 모델 갱신 → 클라이언트 조회 순으로 진행됩니다.

4. 간단한 C# 예제: Todo 관리

예제로 간단한 Todo 애플리케이션에서 CQRS와 Event Sourcing을 적용해보겠습니다.

Command 정의

public record CreateTodoCommand(Guid Id, string Title);

Event 정의

public record TodoCreatedEvent(Guid Id, string Title, DateTime CreatedAt);

Aggregate / Command Handler

public class TodoAggregate
{
    private readonly IEventStore _eventStore;

    public TodoAggregate(IEventStore eventStore)
    {
        _eventStore = eventStore;
    }

    public async Task Handle(CreateTodoCommand command)
    {
        var @event = new TodoCreatedEvent(command.Id, command.Title, DateTime.UtcNow);
        await _eventStore.SaveEvent(@event);
    }
}

Event Store 구현

public interface IEventStore
{
    Task SaveEvent(object @event);
    Task<IEnumerable<object>> GetEvents();
}

public class InMemoryEventStore : IEventStore
{
    private readonly List<object> _events = new();

    public Task SaveEvent(object @event)
    {
        _events.Add(@event);
        return Task.CompletedTask;
    }

    public Task<IEnumerable<object>> GetEvents() => Task.FromResult(_events.AsEnumerable());
}

Read Model 갱신

public class TodoReadModel
{
    private readonly List<TodoDto> _todos = new();

    public void Apply(TodoCreatedEvent @event)
    {
        _todos.Add(new TodoDto { Id = @event.Id, Title = @event.Title, CreatedAt = @event.CreatedAt });
    }

    public IEnumerable<TodoDto> GetAll() => _todos;
}

public class TodoDto
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public DateTime CreatedAt { get; set; }
}

이렇게 하면 이벤트 기반으로 상태가 기록되고, Read 모델은 CQRS 원칙에 따라 별도로 관리됩니다.

5. 실무 적용 팁

  • Command와 Event를 명확히 구분하여 설계
  • Read 모델을 캐싱하거나 별도의 데이터베이스로 분리하면 조회 성능 향상
  • 이벤트 설계 시 버전 관리와 호환성을 고려
  • Message Bus(Kafka, RabbitMQ)와 연계하여 분산 처리 가능
  • 테스트 주도 개발(TDD)과 이벤트 재생으로 안정성 확보

6. 마무리

오늘은 C#에서 CQRS와 Event Sourcing을 이해하고, 간단한 Todo 애플리케이션 예제로 구현 방법을 살펴보았습니다. CQRS를 통해 쓰기/읽기 모델을 분리하고, Event Sourcing으로 상태 변경 이력을 관리하면 복잡한 비즈니스 로직도 안정적으로 처리할 수 있어요. 실무에서는 이벤트 설계와 Read 모델 최적화가 핵심이며, 메시지 기반 아키텍처와 결합하면 강력한 시스템을 만들 수 있습니다. 😊

반응형