1. 이벤트 소싱의 기본 개념
이벤트 소싱의 핵심은 상태 변경을 이벤트로 표현하고, 이 이벤트들의 시퀀스를 저장하는 것입니다. 시스템의 현재 상태는 이 이벤트들을 순서대로 적용하여 재구성됩니다.
1.1 이벤트 모델 정의
public interface Event {
LocalDateTime getTimestamp();
}
public class AccountCreatedEvent implements Event {
private final String accountId;
private final LocalDateTime timestamp;
public AccountCreatedEvent(String accountId) {
this.accountId = accountId;
this.timestamp = LocalDateTime.now();
}
// Getters 추가
}
public class MoneyDepositedEvent implements Event {
private final String accountId;
private final BigDecimal amount;
private final LocalDateTime timestamp;
public MoneyDepositedEvent(String accountId, BigDecimal amount) {
this.accountId = accountId;
this.amount = amount;
this.timestamp = LocalDateTime.now();
}
// Getters 추가
}
팁: 이벤트는 불변(immutable)으로 설계하여 시스템의 일관성을 유지하세요.
2. 이벤트 저장소 구현
이벤트 저장소는 모든 이벤트를 저장하고 검색하는 역할을 합니다.
public interface EventStore {
void saveEvent(String aggregateId, Event event);
List getEvents(String aggregateId);
}
public class InMemoryEventStore implements EventStore {
private final Map<String, List> store = new ConcurrentHashMap<>();
@Override
public void saveEvent(String aggregateId, Event event) {
store.computeIfAbsent(aggregateId, k -> new ArrayList<>()).add(event);
}
@Override
public List getEvents(String aggregateId) {
return store.getOrDefault(aggregateId, Collections.emptyList());
}
}
팁: 실제 프로덕션 환경에서는 데이터베이스나 분산 저장소를 사용하여 이벤트를 영구적으로 저장하세요.
3. 집계(Aggregate) 구현
집계는 관련된 이벤트들을 적용하여 현재 상태를 표현하는 객체입니다.
public class Account {
private String id;
private BigDecimal balance;
public Account(String id) {
this.id = id;
this.balance = BigDecimal.ZERO;
}
public void apply(Event event) {
if (event instanceof AccountCreatedEvent) {
applyAccountCreatedEvent((AccountCreatedEvent) event);
} else if (event instanceof MoneyDepositedEvent) {
applyMoneyDepositedEvent((MoneyDepositedEvent) event);
}
}
private void applyAccountCreatedEvent(AccountCreatedEvent event) {
this.id = event.getAccountId();
}
private void applyMoneyDepositedEvent(MoneyDepositedEvent event) {
this.balance = this.balance.add(event.getAmount());
}
// Getters 추가한다.
}
팁: 집계 객체는 이벤트를 적용하여 상태를 변경하는 메서드만 포함해야 합니다. 비즈니스 로직은 별도의 서비스 레이어에서 처리하세요.
4. 명령 핸들러 구현
명령 핸들러는 비즈니스 로직을 처리하고 새로운 이벤트를 생성합니다.
public class AccountCommandHandler {
private final EventStore eventStore;
public AccountCommandHandler(EventStore eventStore) {
this.eventStore = eventStore;
}
public void handleCreateAccount(String accountId) {
Event event = new AccountCreatedEvent(accountId);
eventStore.saveEvent(accountId, event);
}
public void handleDeposit(String accountId, BigDecimal amount) {
List events = eventStore.getEvents(accountId);
Account account = new Account(accountId);
events.forEach(account::apply);
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive");
}
Event event = new MoneyDepositedEvent(accountId, amount);
eventStore.saveEvent(accountId, event);
}
}
팁: 명령 핸들러에서 비즈니스 규칙을 검증하고, 유효한 경우에만 이벤트를 생성하세요.
5. 쿼리 측 구현 (CQRS 패턴)
이벤트 소싱은 종종 CQRS(Command Query Responsibility Segregation) 패턴과 함께 사용됩니다.
public class AccountQueryService {
private final EventStore eventStore;
public AccountQueryService(EventStore eventStore) {
this.eventStore = eventStore;
}
public Account getAccount(String accountId) {
List events = eventStore.getEvents(accountId);
Account account = new Account(accountId);
events.forEach(account::apply);
return account;
}
}
팁: 실제 애플리케이션에서는 성능 향상을 위해 별도의 읽기 전용 모델을 유지하고 이벤트를 기반으로 이를 업데이트하는 것이 좋습니다.
6. 테스팅
이벤트 소싱 기반 시스템의 테스트 방법을 살펴봅니다.
public class AccountTest {
@Test
public void testAccountCreationAndDeposit() {
EventStore eventStore = new InMemoryEventStore();
AccountCommandHandler commandHandler = new AccountCommandHandler(eventStore);
AccountQueryService queryService = new AccountQueryService(eventStore);
String accountId = "acc123";
commandHandler.handleCreateAccount(accountId);
commandHandler.handleDeposit(accountId, new BigDecimal("100.00"));
Account account = queryService.getAccount(accountId);
assertEquals(new BigDecimal("100.00"), account.getBalance());
}
}
팁: 이벤트 시퀀스를 기반으로 테스트를 작성하면, 시스템의 동작을 더 명확하게 검증할 수 있습니다.
결론
이벤트 소싱은 강력한 패턴이지만, 복잡성을 증가시킬 수 있습니다. 시스템의 요구사항과 규모를 고려하여 적용 여부를 결정해야 합니다. 이벤트 소싱은 감사, 디버깅, 시스템 상태 재구성 등에 큰 이점을 제공하며, 특히 복잡한 도메인 모델을 다루는 시스템에서 유용합니다.