IT/JAVA

Java에서 Event-Driven Architecture 구현: 확장 가능한 시스템 설계

KeepGooing 2024. 11. 30. 13:36
반응형

1. EDA의 핵심 개념

EDA는 이벤트 생성자(producer), 이벤트 채널, 이벤트 소비자(consumer)로 구성됩니다.

1.1 이벤트 모델 정의


public class Event {
    private String type;
    private T data;
    private LocalDateTime timestamp;

    public Event(String type, T data) {
        this.type = type;
        this.data = data;
        this.timestamp = LocalDateTime.now();
    }

    // Getters and setters
}
        

팁: 이벤트 모델을 설계할 때는 확장성을 고려하세요. 제네릭을 사용하면 다양한 유형의 데이터를 처리할 수 있습니다.

2. 이벤트 버스 구현

이벤트 버스는 이벤트 생성자와 소비자 사이의 중개자 역할을 합니다.


public class EventBus {
    private final Map<String, List> handlers = new ConcurrentHashMap<>();

    public void subscribe(String eventType, EventHandler handler) {
        handlers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()).add(handler);
    }

    public void publish(Event<?> event) {
        List eventHandlers = handlers.get(event.getType());
        if (eventHandlers != null) {
            eventHandlers.forEach(handler -> handler.handle(event));
        }
    }
}

@FunctionalInterface
public interface EventHandler {
    void handle(Event<?> event);
}
        

팁: ConcurrentHashMap과 CopyOnWriteArrayList를 사용하여 스레드 안전성을 보장하세요. 이는 동시에 여러 스레드에서 이벤트를 발행하고 구독할 수 있게 해줍니다.

3. 비동기 이벤트 처리

대규모 시스템에서는 비동기 이벤트 처리가 필수적입니다.


public class AsyncEventBus {
    private final EventBus eventBus;
    private final ExecutorService executorService;

    public AsyncEventBus(int threadPoolSize) {
        this.eventBus = new EventBus();
        this.executorService = Executors.newFixedThreadPool(threadPoolSize);
    }

    public void subscribe(String eventType, EventHandler handler) {
        eventBus.subscribe(eventType, event -> 
            executorService.submit(() -> handler.handle(event)));
    }

    public void publish(Event<?> event) {
        executorService.submit(() -> eventBus.publish(event));
    }

    public void shutdown() {
        executorService.shutdown();
    }
}
        

팁: ExecutorService를 사용하여 이벤트 처리를 비동기적으로 수행하세요. 이는 시스템의 응답성을 높이고 처리량을 증가시킵니다.

4. 이벤트 소싱 구현

이벤트 소싱은 상태 변경을 일련의 이벤트로 저장하는 패턴입니다.


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

    public void store(Event<?> event) {
        events.add(event);
    }

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

    public void replay(EventHandler handler) {
        events.forEach(handler::handle);
    }
}

public class BankAccount {
    private String id;
    private double balance;

    public void apply(Event<?> event) {
        if (event.getType().equals("DEPOSIT")) {
            this.balance += (Double) event.getData();
        } else if (event.getType().equals("WITHDRAW")) {
            this.balance -= (Double) event.getData();
        }
    }

    public void reconstruct(EventStore eventStore) {
        eventStore.replay(this::apply);
    }
}
        

팁: 이벤트 소싱을 사용하면 시스템의 모든 상태 변경을 추적할 수 있으며, 특정 시점의 상태를 쉽게 재구성할 수 있습니다.

5. 통합 패턴

EDA를 외부 시스템과 통합하는 방법을 살펴봅니다.

5.1 메시지 브로커 통합


@Service
public class KafkaEventPublisher {
    @Autowired
    private KafkaTemplate<String, Event<?>> kafkaTemplate;

    public void publish(Event<?> event) {
        kafkaTemplate.send("events-topic", event);
    }
}

@Service
public class KafkaEventConsumer {
    @Autowired
    private EventBus eventBus;

    @KafkaListener(topics = "events-topic")
    public void consume(Event<?> event) {
        eventBus.publish(event);
    }
}
        

팁: Apache Kafka와 같은 메시지 브로커를 사용하면 시스템 간 이벤트 교환을 안정적으로 수행할 수 있습니다. 이는 마이크로서비스 아키텍처에서 특히 유용합니다.

6. EDA 테스팅

이벤트 기반 시스템의 테스트 전략을 살펴봅니다.


@RunWith(MockitoJUnitRunner.class)
public class EventBusTest {
    @Mock
    private EventHandler mockHandler;

    @Test
    public void testEventPublishAndSubscribe() {
        EventBus eventBus = new EventBus();
        Event testEvent = new Event<>("TEST", "Test Data");

        eventBus.subscribe("TEST", mockHandler);
        eventBus.publish(testEvent);

        verify(mockHandler, times(1)).handle(testEvent);
    }
}
        

팁: 단위 테스트와 통합 테스트를 조합하여 EDA 시스템을 철저히 테스트하세요. 모의 객체(Mock)를 사용하여 이벤트 핸들러의 동작을 검증할 수 있습니다.

결론

Event-Driven Architecture는 Java 애플리케이션의 확장성과 유연성을 크게 향상시킬 수 있습니다. 이벤트 버스, 비동기 처리, 이벤트 소싱 등의 기술을 적절히 조합하여 사용하면, 복잡한 비즈니스 요구사항을 효과적으로 처리할 수 있는 시스템을 구축할 수 있습니다. 그러나 EDA 구현 시에는 이벤트의 일관성, 순서 보장, 장애 처리 등의 도전 과제도 고려해야 합니다.

추가로 EDA를 구현할 때는 도메인 주도 설계(DDD) 원칙을 함께 적용하는 것이 좋습니다. 이벤트를 도메인 모델의 중요한 부분으로 취급하고, 유비쿼터스 언어를 사용하여 이벤트를 명명하세요. 또한, 이벤트 스키마의 버전 관리와 이벤트 발행/구독의 역할 분리에도 주의를 기울이세요. 마지막으로, 분산 시스템에서의 일관성 유지를 위해 SAGA 패턴이나 이벤트 소싱과 CQRS를 함께 고려해보는 것도 좋은 방법입니다.

반응형