Event Sourcing
이벤트 소싱 이란?
이벤트 소싱Event Sourcing은 시스템 상태를 변경하는 모든 이벤트Event를 저장하고, 이러한 이벤트들을 기반으로 시스템의 상태를 재구성하는 패턴입니다. 즉, 데이터를 직접 저장하는 대신, 그 데이터에 대한 상태 변화를 유발하는 이벤트를 저장하고, 이 이벤트의 흐름을 재생Replay하여 시스템의 상태를 복원합니다. 이벤트 소싱 패턴은 전통적인 CRUDCreate, Read, Update, Delete 방식과는 다릅니다. CRUD에서는 데이터의 현재 상태만을 저장하지만, 이벤트 소싱은 상태 변화를 일으키는 모든 이벤트를 저장하므로 과거의 모든 변경 내역을 추적할 수 있습니다.
주요 개념
이벤트Event
시스템에서 발생하는 모든 상태 변화의 기록입니다. 예를 들어, 사용자가 도서를 대출한 기록은 “도서 대출됨(Book Borrowed)“이라는 이벤트로 저장될 수 있습니다.
이벤트 로그Event Log
이벤트들이 저장되는 연속된 저장소입니다. 이 이벤트 로그에는 모든 이벤트가 순차적으로 기록되며, 이는 불변Immutable입니다. 즉, 이미 발생한 이벤트는 수정되거나 삭제되지 않습니다.
현재 상태Current State
현재 시스템의 상태는 모든 이벤트를 순차적으로 재생하여 계산된 결과입니다. 시스템이 재시작되면 이벤트 로그를 재생하여 상태를 다시 복원할 수 있습니다.
이벤트 소싱의 장점
- 완벽한 감사 로그Audit Log: 모든 상태 변화는 이벤트로 기록되므로, 모든 변경 내역을 정확히 추적할 수 있습니다. 과거의 시스템 상태를 정확히 재현할 수 있으며, 이를 통해 디버깅이나 감사가 용이해집니다.
- 이벤트 기반 통합: 이벤트는 상태 변화를 나타내므로, 시스템 간의 통합 시 이벤트 기반 메시징을 통해 자연스럽게 연동할 수 있습니다.
- 타임 트래블Time Travel: 이벤트 로그를 이용해 시스템의 과거 상태를 재현하거나, 특정 시점의 상태를 확인할 수 있습니다.
- 확장성: 이벤트는 독립적이므로 분산 환경에서 확장성이 뛰어납니다. 이벤트를 적재한 후 다른 시스템에 비동기적으로 전파할 수 있습니다.
이벤트 소싱의 단점
- 복잡성: 모든 상태 변경을 이벤트로 기록해야 하므로, 단순한 CRUD 방식에 비해 시스템 설계가 복잡해질 수 있습니다.
- 성능 문제: 시스템이 재시작될 때 이벤트 로그를 모두 재생해야 현재 상태를 복원할 수 있기 때문에, 로그가 길어질 경우 성능 저하가 발생할 수 있습니다. 이를 해결하기 위해 스냅샷Snapshot을 사용하는 방법이 있습니다.
- 데이터 모델링: 이벤트 중심의 데이터 모델링은 기존의 CRUD 방식에 비해 익숙하지 않을 수 있으며, 이를 도입하는 데 학습 곡선이 필요할 수 있습니다.
예시: 도서 관리 시스템에서의 이벤트 소싱
도서 관리 시스템을 예로 들어, 사용자가 도서를 대출하거나 반납하는 모든 상태 변화를 이벤트로 저장할 수 있습니다.
이벤트 정의
public class BookBorrowed
{
public string BookId { get; set; }
public string UserId { get; set; }
public DateTime BorrowedDate { get; set; }
public BookBorrowed(string bookId, string userId)
{
BookId = bookId;
UserId = userId;
BorrowedDate = DateTime.Now;
}
}
public class BookReturned
{
public string BookId { get; set; }
public string UserId { get; set; }
public DateTime ReturnedDate { get; set; }
public BookReturned(string bookId, string userId)
{
BookId = bookId;
UserId = userId;
ReturnedDate = DateTime.Now;
}
}
이벤트 저장소
public class EventStore
{
private List<object> _events = new List<object>();
public void SaveEvent(object @event)
{
_events.Add(@event);
Console.WriteLine($"Event saved: {@event.GetType().Name}");
}
public List<object> GetEvents() => _events;
}
시스템 상태 재생
public class BookState
{
public string BookId { get; set; }
public string UserId { get; set; }
public bool IsBorrowed { get; set; }
public void Apply(BookBorrowed @event)
{
BookId = @event.BookId;
UserId = @event.UserId;
IsBorrowed = true;
}
public void Apply(BookReturned @event)
{
BookId = @event.BookId;
UserId = @event.UserId;
IsBorrowed = false;
}
}
public class EventProcessor
{
private BookState _bookState = new BookState();
public void ProcessEvents(List<object> events)
{
foreach (var @event in events)
{
switch (@event)
{
case BookBorrowed borrowed:
_bookState.Apply(borrowed);
break;
case BookReturned returned:
_bookState.Apply(returned);
break;
}
}
}
public BookState GetCurrentState() => _bookState;
}
예시 실행
public class Program
{
public static void Main(string[] args)
{
var eventStore = new EventStore();
// 이벤트 기록
eventStore.SaveEvent(new BookBorrowed("Book1", "User1"));
eventStore.SaveEvent(new BookReturned("Book1", "User1"));
// 이벤트 재생을 통한 상태 복원
var eventProcessor = new EventProcessor();
eventProcessor.ProcessEvents(eventStore.GetEvents());
// 현재 상태 확인
var currentState = eventProcessor.GetCurrentState();
Console.WriteLine($"Book ID: {currentState.BookId}, IsBorrowed: {currentState.IsBorrowed}");
}
}
스냅샷 활용
이벤트가 너무 많아질 경우, 모든 이벤트를 재생하는 것이 비효율적일 수 있습니다. 이를 해결하기 위해, 특정 시점의 상태를 스냅샷Snapshot으로 저장한 후, 그 이후의 이벤트만 재생하는 방법을 사용할 수 있습니다.
스냅샷 저장
public class Snapshot
{
public string BookId { get; set; }
public string UserId { get; set; }
public bool IsBorrowed { get; set; }
public DateTime Timestamp { get; set; }
}
이렇게 스냅샷을 사용하면 이벤트 로그가 매우 길어질 때도 성능을 개선할 수 있습니다.
맺음말
이벤트 소싱은 시스템의 모든 상태 변화를 기록하고, 이를 통해 과거의 상태를 복원할 수 있는 매우 강력한 패턴입니다. 특히, 데이터 무결성과 이벤트 기반 통합이 중요한 도메인에서 유용하게 사용할 수 있습니다. 그러나 복잡성과 성능 문제를 잘 고려해야 하며, 스냅샷과 같은 기술을 적절히 사용하여 이를 해결할 수 있습니다.
심화 학습
이벤트 소싱은 단순한 CRUD 방식과 비교하여 매우 강력한 기능을 제공하지만, 이를 효과적으로 사용하기 위해서는 몇 가지 고급 기법과 고려 사항이 필요합니다. 아래에서는 이벤트 소싱의 심화 개념, 패턴 적용 시의 도전과 문제 해결 방법에 대해 다룹니다.
이벤트 소싱과 CQRS의 통합
CQRSCommand Query Responsibility Segregation와 이벤트 소싱은 자주 함께 사용됩니다. CQRS는 명령Command과 조회Query를 분리하여 시스템의 쓰기 작업과 읽기 작업을 분리된 모델로 처리하는 패턴입니다. 이벤트 소싱은 시스템의 상태 변화를 이벤트로 저장하는 반면, CQRS는 이를 통해 쓰기와 읽기를 분리하고 성능을 최적화할 수 있습니다.
- 쓰기 모델Command Model: 이벤트가 발생하고 저장되는 과정은 쓰기 모델에서 처리됩니다. 이벤트가 발생하면 그 이벤트가 저장되고, 이를 통해 시스템 상태가 업데이트됩니다.
- 읽기 모델Query Model: 읽기 모델에서는 이벤트 로그를 통해 필요한 데이터를 재구성하여, 빠르고 최적화된 조회 성능을 제공합니다.
예시: CQRS와 이벤트 소싱의 통합
public class BorrowBookCommand
{
public string BookId { get; set; }
public string UserId { get; set; }
}
public class BookBorrowedEvent
{
public string BookId { get; set; }
public string UserId { get; set; }
public DateTime BorrowedDate { get; set; }
public BookBorrowedEvent(string bookId, string userId)
{
BookId = bookId;
UserId = userId;
BorrowedDate = DateTime.Now;
}
}
// CommandHandler: Command를 처리하고 이벤트를 생성
public class BorrowBookCommandHandler
{
private EventStore _eventStore;
public BorrowBookCommandHandler(EventStore eventStore)
{
_eventStore = eventStore;
}
public void Handle(BorrowBookCommand command)
{
var @event = new BookBorrowedEvent(command.BookId, command.UserId);
_eventStore.SaveEvent(@event);
}
}
여기서는 CQRS의 Command가 이벤트를 발생시키고, 이벤트 저장소에 저장되는 방식으로 작동합니다. 읽기 모델에서는 이 이벤트를 기반으로 데이터를 빠르게 조회할 수 있습니다.
이벤트 버스
이벤트 소싱에서는 이벤트를 저장소에 저장하는 것뿐만 아니라, 다른 시스템이나 모듈이 해당 이벤트를 구독하고 반응할 수 있도록 해야 합니다. 이를 위해 이벤트 버스Event Bus라는 패턴이 자주 사용됩니다. 이벤트 버스는 발생한 이벤트를 다른 서비스 또는 모듈에 전파하는 역할을 합니다. 이벤트 버스는 특히 분산 시스템에서 유용하며, 마이크로서비스 환경에서 서비스 간의 통신에 자주 사용됩니다. 각 서비스는 이벤트 버스를 통해 이벤트를 구독subscribe하고, 해당 이벤트가 발생할 때 비동기적으로 반응할 수 있습니다.
예시: 이벤트 버스 사용
public interface IEventBus
{
void Publish<T>(T @event);
void Subscribe<T>(Action<T> handler);
}
public class SimpleEventBus : IEventBus
{
private readonly Dictionary<Type, List<object>> _handlers = new();
public void Publish<T>(T @event)
{
var eventType = typeof(T);
if (_handlers.ContainsKey(eventType))
{
foreach (var handler in _handlers[eventType])
{
((Action<T>)handler)(@event);
}
}
}
public void Subscribe<T>(Action<T> handler)
{
var eventType = typeof(T);
if (!_handlers.ContainsKey(eventType))
{
_handlers[eventType] = new List<object>();
}
_handlers[eventType].Add(handler);
}
}
위 코드에서, SimpleEventBus
는 이벤트가 발생하면 구독자들에게 전파하는 역할을 합니다. 이를 통해 각 모듈이나 서비스가 비동기적으로 이벤트에 반응할 수 있습니다.
이벤트 버전 관리
이벤트 소싱에서 중요한 고려 사항 중 하나는 이벤트의 버전 관리입니다. 시스템이 발전하면서 이벤트의 구조가 변경될 수 있으며, 이 경우 이전에 기록된 이벤트와 호환성을 유지해야 합니다. 이를 해결하기 위한 몇 가지 방법이 있습니다.
- 이벤트 버전 추가: 이벤트마다 버전 번호를 부여하여, 특정 버전의 이벤트가 발생했을 때 이에 맞는 처리 로직을 적용합니다.
public class BookBorrowedV1
{
public string BookId { get; set; }
public string UserId { get; set; }
}
public class BookBorrowedV2
{
public string BookId { get; set; }
public string UserId { get; set; }
public DateTime BorrowedDate { get; set; }
}
- 이벤트 변환기Event Upgrader: 이전 버전의 이벤트를 새로운 버전으로 변환하는 변환기를 작성하여 호환성을 유지합니다.
public class EventUpgrader
{
public BookBorrowedV2 Upgrade(BookBorrowedV1 oldEvent)
{
return new BookBorrowedV2
{
BookId = oldEvent.BookId,
UserId = oldEvent.UserId,
BorrowedDate = DateTime.Now
};
}
}
이 방법을 사용하면, 과거 이벤트를 새로운 형식으로 변환하여 처리할 수 있습니다.
성능 최적화: 스냅샷
앞서 언급했듯이, 이벤트 소싱에서는 이벤트 로그를 모두 재생하여 상태를 복원하는 데 시간이 오래 걸릴 수 있습니다. 이 문제를 해결하기 위해 스냅샷Snapshot을 활용할 수 있습니다. 스냅샷은 특정 시점의 시스템 상태를 저장하고, 그 이후의 이벤트만 재생하여 성능을 최적화하는 방법입니다.
스냅샷 생성 및 사용
public class Snapshot
{
public string BookId { get; set; }
public string UserId { get; set; }
public bool IsBorrowed { get; set; }
public DateTime SnapshotDate { get; set; }
}
public class SnapshotStore
{
private Snapshot _latestSnapshot;
public void SaveSnapshot(Snapshot snapshot)
{
_latestSnapshot = snapshot;
Console.WriteLine("Snapshot saved.");
}
public Snapshot GetLatestSnapshot()
{
return _latestSnapshot;
}
}
스냅샷은 이벤트 로그를 모두 재생하지 않고도 최신 상태를 빠르게 복원할 수 있는 방법을 제공합니다. 이를 통해 이벤트 로그가 너무 길어졌을 때도 성능 문제를 해결할 수 있습니다.
이벤트 소싱의 장애 처리 및 복구
이벤트 소싱에서 이벤트가 손실되거나 잘못 저장될 경우 시스템의 상태가 일관되지 않을 수 있습니다. 이를 방지하기 위한 장애 처리 및 복구 전략이 필요합니다.
- 이벤트 무결성 보장: 이벤트를 저장할 때는 트랜잭션을 사용하여, 이벤트 저장이 실패할 경우 롤백할 수 있도록 합니다.
- 이벤트 중복 처리: 이벤트가 중복으로 처리되지 않도록, 각 이벤트에 고유한 ID를 부여하고 중복을 감지하는 로직을 추가합니다.
- 이벤트 재처리: 장애가 발생했을 때 특정 시점으로 돌아가 이벤트를 다시 처리하여 시스템 상태를 복구할 수 있도록 이벤트 재처리 메커니즘을 구축합니다.