IT/JAVA

Java에서 이벤트 소싱(Event Sourcing) 패턴 구현하기

KeepGooing 2024. 11. 30. 08:29
반응형

Java에서 이벤트 소싱(Event Sourcing) 패턴 구현하기

 

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());
    }
}
        

팁: 이벤트 시퀀스를 기반으로 테스트를 작성하면, 시스템의 동작을 더 명확하게 검증할 수 있습니다.

결론

이벤트 소싱은 강력한 패턴이지만, 복잡성을 증가시킬 수 있습니다. 시스템의 요구사항과 규모를 고려하여 적용 여부를 결정해야 합니다. 이벤트 소싱은 감사, 디버깅, 시스템 상태 재구성 등에 큰 이점을 제공하며, 특히 복잡한 도메인 모델을 다루는 시스템에서 유용합니다.

추가 팁: 이벤트 소싱을 구현할 때는 이벤트 스키마 버전 관리, 스냅샷 생성, 이벤트 재생 최적화 등의 고급 주제도 고려해야 합니다. 또한, 대규모 시스템에서는 이벤트 저장소의 성능과 확장성이 중요한 고려사항이 됩니다. Apache Kafka나 Event Store와 같은 특화된 도구를 활용하는 것도 좋은 방법입니다. 마지막으로, 이벤트 소싱은 학습 곡선이 있으므로 팀 전체가 이 패턴을 이해하고 효과적으로 사용할 수 있도록 충분한 교육과 연습이 필요합니다.

반응형