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, 사가 등의 패턴을 적절히 조합하여 사용하면, 복잡한 비즈니스 요구사항을 효과적으로 처리할 수 있는 시스템을 구축할 수 있습니다. 그러나 이러한 아키텍처의 복잡성을 관리하기 위해서는 신중한 설계와 지속적인 모니터링, 그리고 체계적인 테스트가 필수적입니다.