인프라/MSA

[MSA] 분산 트랜잭션 - 이벤트 소싱

캣코딩 2023. 12. 20. 09:21

이벤트 소싱 (Event Sourcing)

출처: https://medium.com

 

분산 시스템에서 데이터 상태를 변경하는데 사용되는 디자인 패턴 중 하나입니다. 이 패턴은 시스템의 상태 변경을 이벤트의 연속된 흐름으로 저장하고, 이 이벤트를 기반으로 현재 상태를 재구성합니다. 이벤트 소싱은 주로 마이크로서비스 아키텍처와 같은 분산 시스템에서 데이터 일관성과 확장성을 유지하는데 활용됩니다.

 

이벤트 소싱의 원리

1. 이벤트 기록

시스템에서 발생하는 모든 상태 변경은 이벤트로 기록됩니다. 각 이벤트는 시간 순서대로 기록되며, 데이터베이스에는 현재 상태가 아니라 이벤트의 시퀀스가 저장됩니다.

 

2. 상태 재구성

현재 상태를 얻기 위해 이벤트의 시퀀스를 사용하여 상태를 재구성합니다. 이는 이벤트를 순차적으로 반복하면서 현재 상태를 만들어내는 과정을 의미합니다.

 

3. 유연한 데이터 모델

이벤트 소싱은 이벤트에 대한 데이터 모델을 사용하므로 데이터 구조를 유연하게 설계할 수 있습니다. 새로운 이벤트를 추가하거나 기존 이벤트를 수정하는 것이 간단하며, 이로 인해 시스템이 개선되거나 변경될 때 더 나은 유연성을 제공합니다.

 

4. 이벤트 스트림

이벤트는 일반적으로 이벤트 스트림 형태로 저장됩니다. 이벤트 스트림은 시스템에서 발생하는 모든 이벤트를 포함하며, 각 이벤트는 고유한 식별자를 가지고 있습니다.

 

이벤트 소싱의 이점

1. 데이터 일관성

이벤트 소싱은 각 이벤트의 시퀀스를 사용하여 현재 상태를 재구성하므로 데이터 일관성을 유지하기 쉽습니다. 모든 변경은 이벤트로 기록되고, 이벤트 스트림은 항상 일관된 순서로 저장됩니다.

 

2. 이력 추적과 감사

시스템의 상태가 변경될 때마다 이벤트가 기록되므로 언제든지 특정 시점의 시스템 상태로 돌아갈 수 있습니다. 이는 디버깅, 감사, 복구 등에 유용합니다.

 

3. 이벤트 기반 아키텍처와 통합

이벤트 소싱은 이벤트 기반 아키텍처와 잘 통합됩니다. 이벤트 스트림은 여러 마이크로서비스 간에 데이터를 전파하고 통합하는 데 사용될 수 있습니다.

 

이벤트 소싱의 한계

1. 시스템의 복잡성 증가

이벤트 소싱은 시스템에 추가적인 구성 및 관리를 필요로 하며, 이는 전반적인 시스템의 복잡성을 증가시킬 수 있습니다. 이는 특히 강력한 일관성과 안정성이 필요하지 않은 간단한 애플리케이션에서는 불필요한 오버헤드일 수 있습니다.

 

2. 이벤트 스트림 관리

이벤트 스트림은 매우 길어질 수 있으며, 이를 효과적으로 관리하기 위해서는 데이터베이스 또는 이벤트 스토어의 성능과 확장성이 중요합니다. 특히, 이벤트 스트림을 효과적으로 질의하고 인덱싱하는 것이 복잡할 수 있습니다.

 

이벤트 소싱의 대안과 고려사항

1. CQRS 패턴 활용

CQRS 패턴은 이벤트 소싱과 함께 사용될 수 있습니다. CQRS는 읽기와 쓰기 작업을 분리하여 각각의 요구 사항에 맞게 최적화된 모델을 사용합니다.

 

2. 스냅샷 및 이벤트 정리

시간이 지남에 따라 이벤트 스트림이 커질 수 있습니다. 스냅샷을 사용하여 특정 지점에서 상태를 저장하고 이후의 이벤트는 버림으로써 성능을 향상시킬 수 있습니다.

 

3. 분산 트랜잭션 적용

특정 상황에서는 분산 트랜잭션을 적용할 수 있습니다. 이는 트랜잭션의 일관성을 보장하면서도 이벤트 소싱의 유연성을 제공합니다. 다만, 분산 트랜잭션은 일부 환경에서는 복잡성을 증가시킬 수 있습니다.

 

이벤트 소싱의 간단한 예시

이 예시에서는 Spring Boot와 Spring Data JPA를 사용하여 이벤트 소싱을 시뮬레이션 합니다.

이 코드는 간단한 Spring Boot 애플리케이션으로, 실행하면 잔고 변경 이벤트가 데이터베이스에 저장되고 조회되는 간단한 시뮬레이션을 수행합니다. 이벤트 소싱을 더 복잡한 시나리오에서 활용하기 위해서는 이벤트 스토어, CQRS, 스냅샷 등의 패턴과 개념을 추가로 고려해야 할 것 입니다.

 

@Entity
@Getter
@Setter
@NoArgsConstructor
class BalanceEvent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String accountId;
    private double amount;
}

@Repository
interface BalanceEventRepository extends JpaRepository<BalanceEvent, Long> {
    // 특정 계좌의 잔고 변경 이벤트를 조회하는 메서드
    List<BalanceEvent> findByAccountId(String accountId);
}

@Service
@RequiredArgsConstructor
class BalanceEventHandler {
    private final BalanceEventRepository eventRepository;

    @Transactional
    public void handleEvent(BalanceEvent event) {
        // 잔고 변경 이벤트를 저장
        eventRepository.save(event);
    }

    public List<BalanceEvent> getEventsByAccountId(String accountId) {
        // 특정 계좌의 모든 잔고 변경 이벤트를 조회
        return eventRepository.findByAccountId(accountId);
    }
}

@Service
@RequiredArgsConstructor
class AccountService {
    private final BalanceEventHandler eventHandler;

    @Transactional
    public void changeBalance(String accountId, double amount) {
        // 새로운 잔고 변경 이벤트 생성 및 처리
        BalanceEvent event = new BalanceEvent();
        event.setAccountId(accountId);
        event.setAmount(amount);

        eventHandler.handleEvent(event);
    }

    public double getCurrentBalance(String accountId) {
        // 특정 계좌의 현재 잔고를 계산하여 반환
        List<BalanceEvent> balanceEvents = eventHandler.getEventsByAccountId(accountId);
        double currentBalance = 0.0;

        for (BalanceEvent event : balanceEvents) {
            currentBalance += event.getAmount();
        }
        return currentBalance;
    }
}