IT/JAVA

Java 이벤트 기반 아키텍처: 구현 패턴과 모범 사례

KeepGooing 2024. 12. 11. 12:17
반응형

Java 이벤트 기반 아키텍처: 구현 패턴과 모범 사례

 

1. 발행-구독(Publish-Subscribe) 패턴

발행-구독 패턴은 EDA의 핵심 패턴 중 하나로, 이벤트 생산자와 소비자를 분리합니다.

public interface EventPublisher {
    void publish(Event event);
}

public interface EventSubscriber {
    void onEvent(Event event);
}

public class SimpleEventBus implements EventPublisher {
    private final Map<Class<? extends Event>, List> subscribers = new ConcurrentHashMap<>();

    public void subscribe(Class<? extends Event> eventType, EventSubscriber subscriber) {
        subscribers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()).add(subscriber);
    }

    @Override
    public void publish(Event event) {
        List eventSubscribers = subscribers.get(event.getClass());
        if (eventSubscribers != null) {
            eventSubscribers.forEach(subscriber -> subscriber.onEvent(event));
        }
    }
}

 

Tip: 발행-구독 패턴을 구현할 때는 스레드 안전성에 주의를 기울이세요. ConcurrentHashMap과 CopyOnWriteArrayList를 사용하면 동시성 문제를 피할 수 있습니다.

2. 이벤트 소싱(Event Sourcing) 패턴

이벤트 소싱은 애플리케이션의 상태 변화를 일련의 이벤트로 저장하는 패턴입니다.

public class EventStore {
    private final List events = new ArrayList<>();

    public void saveEvent(Event event) {
        events.add(event);
    }

    public List getEvents() {
        return new ArrayList<>(events);
    }
}

public class Account {
    private String id;
    private BigDecimal balance;

    public void apply(AccountCreatedEvent event) {
        this.id = event.getAccountId();
        this.balance = event.getInitialBalance();
    }

    public void apply(MoneyDepositedEvent event) {
        this.balance = this.balance.add(event.getAmount());
    }

    public void replay(List events) {
        events.forEach(event -> {
            if (event instanceof AccountCreatedEvent) {
                apply((AccountCreatedEvent) event);
            } else if (event instanceof MoneyDepositedEvent) {
                apply((MoneyDepositedEvent) event);
            }
        });
    }
}
        

Tip: 이벤트 소싱을 구현할 때는 이벤트의 불변성을 보장하고, 버전 관리를 고려하세요. 또한, 스냅샷 메커니즘을 도입하여 재생 성능을 개선할 수 있습니다.

3. CQRS(Command Query Responsibility Segregation) 패턴

CQRS는 명령(쓰기)과 조회(읽기) 책임을 분리하는 패턴으로, 이벤트 소싱과 함께 자주 사용됩니다.

public class AccountCommandHandler {
    private final EventStore eventStore;

    public AccountCommandHandler(EventStore eventStore) {
        this.eventStore = eventStore;
    }

    public void handle(CreateAccountCommand command) {
        AccountCreatedEvent event = new AccountCreatedEvent(command.getAccountId(), command.getInitialBalance());
        eventStore.saveEvent(event);
    }

    public void handle(DepositMoneyCommand command) {
        MoneyDepositedEvent event = new MoneyDepositedEvent(command.getAccountId(), command.getAmount());
        eventStore.saveEvent(event);
    }
}

public class AccountQueryHandler {
    private final Map<String, Account> accounts = new ConcurrentHashMap<>();

    public AccountQueryHandler(EventStore eventStore) {
        eventStore.getEvents().forEach(this::apply);
    }

    private void apply(Event event) {
        if (event instanceof AccountCreatedEvent) {
            AccountCreatedEvent e = (AccountCreatedEvent) event;
            accounts.put(e.getAccountId(), new Account(e.getAccountId(), e.getInitialBalance()));
        } else if (event instanceof MoneyDepositedEvent) {
            MoneyDepositedEvent e = (MoneyDepositedEvent) event;
            Account account = accounts.get(e.getAccountId());
            account.setBalance(account.getBalance().add(e.getAmount()));
        }
    }

    public Account getAccount(String accountId) {
        return accounts.get(accountId);
    }
}
        

Tip: CQRS를 구현할 때는 명령 측과 조회 측의 데이터 일관성을 유지하는 것이 중요합니다. 이벤트를 사용하여 두 측면을 동기화하고, 최종 일관성(Eventual Consistency)을 고려하세요.

4. 사가(Saga) 패턴

사가 패턴은 분산 트랜잭션을 관리하기 위한 패턴으로, 일련의 로컬 트랜잭션으로 구성됩니다.

public interface SagaStep {
    CompletableFuture execute(T data);
    CompletableFuture compensate(T data);
}

public class OrderSaga {
    private final List<SagaStep> steps;

    public OrderSaga(List<SagaStep> steps) {
        this.steps = steps;
    }

    public CompletableFuture execute(OrderData initialData) {
        return steps.stream()
                .reduce(CompletableFuture.completedFuture(initialData),
                        (future, step) -> future.thenCompose(step::execute),
                        (f1, f2) -> f1.thenCombine(f2, (o1, o2) -> o2))
                .exceptionally(ex -> {
                    rollback(initialData, steps.indexOf(ex));
                    throw new RuntimeException("Saga failed", ex);
                });
    }

    private void rollback(OrderData data, int lastSuccessfulStep) {
        List<SagaStep> compensatingSteps = steps.subList(0, lastSuccessfulStep + 1);
        Collections.reverse(compensatingSteps);
        compensatingSteps.stream()
                .reduce(CompletableFuture.completedFuture(data),
                        (future, step) -> future.thenCompose(step::compensate),
                        (f1, f2) -> f1.thenCombine(f2, (o1, o2) -> o2));
    }
}
        

Tip: 사가 패턴을 구현할 때는 각 단계의 멱등성을 보장하고, 보상 트랜잭션을 신중히 설계하세요. 또한, 사가의 진행 상태를 지속적으로 추적하고 모니터링하는 메커니즘을 구현하는 것이 좋습니다.

5. 이벤트 기반 아키텍처 모범 사례

  • 이벤트 스키마 버전 관리: 이벤트 구조 변경 시 하위 호환성을 유지하세요.
  • 멱등성 보장: 동일한 이벤트가 여러 번 처리되어도 안전하도록 설계하세요.
  • 비동기 처리: 가능한 한 비동기 처리를 활용하여 시스템의 응답성을 높이세요.
  • 모니터링 및 추적: 분산 추적 시스템을 구현하여 이벤트 흐름을 모니터링하세요.
  • 장애 대응: 회복력 있는 시스템을 위해 서킷 브레이커, 재시도 메커니즘 등을 구현하세요.
  • 테스트 자동화: 단위 테스트부터 통합 테스트, 계약 테스트까지 다양한 수준의 테스트를 자동화하세요.

결론

이벤트 기반 아키텍처는 확장성, 유연성, 그리고 복원력 있는 시스템을 구축하는 데 강력한 도구입니다. 발행-구독, 이벤트 소싱, CQRS, 사가 등의 패턴을 적절히 조합하여 사용하면, 복잡한 비즈니스 요구사항을 효과적으로 처리할 수 있는 시스템을 구축할 수 있습니다. 그러나 이러한 아키텍처의 복잡성을 관리하기 위해서는 신중한 설계와 지속적인 모니터링, 그리고 체계적인 테스트가 필수적입니다.

추가 팁: 이벤트 기반 아키텍처를 구현할 때는 도메인 주도 설계(DDD) 원칙을 함께 적용하는 것이 좋습니다. 이벤트 스토밍 워크샵을 통해 도메인 이벤트를 식별하고, 바운디드 컨텍스트를 정의하세요. 또한, 이벤트 기반 시스템의 복잡성을 관리하기 위해 이벤트 카탈로그를 유지하고, 이벤트 버전 관리 전략을 수립하세요. 마지막으로, 성능 테스트를 통해 시스템의 확장성을 검증하고, 필요에 따라 이벤트 스트리밍 플랫폼(예: Apache Kafka)의 도입을 고려하세요.

 

 

반응형