이벤트 드리븐 아키텍처의 약속과 프로덕션의 현실
이벤트 드리븐 아키텍처(EDA)는 설계 단계에서는 매력적이다. 서비스 간 결합도를 낮추고, 수평 확장이 자연스럽게 가능하며, 비동기 처리로 처리량을 극대화할 수 있다는 약속을 담고 있다. 그러나 Java/Kafka 기반의 컨택센터 플랫폼을 80,000 BHCC(시간당 최대 통화 처리량), 10,000 에이전트 규모로 실운영하다 보면, 설계 단계에서는 보이지 않던 트레이드오프가 하나씩 수면 위로 올라온다. 이 글은 그 현실적인 장애 패턴과 해결책을 다룬다.
대규모 실시간 시스템에서 실제로 만나는 병목
상태 관리의 복잡성은 EDA에서 가장 먼저 드러나는 문제다. 이벤트는 본질적으로 무상태(stateless)지만, 실시간 시스템은 에이전트의 현재 상태, 통화 세션, 큐 우선순위 같은 상태 정보를 지속적으로 추적해야 한다. 이 상태를 여러 컨슈머가 분산해서 처리하는 순간, 정합성 문제가 발생한다. 해결책으로는 Redis를 중앙 상태 저장소로 활용하는 패턴이 효과적이다. 각 컨슈머가 이벤트를 처리한 뒤 Redis에 원자적으로 상태를 업데이트하고, 다음 컨슈머가 이를 읽어가는 구조다.
Kafka 파티션 한계도 실운영에서 자주 마주치는 병목이다. 파티션 수는 병렬 처리량의 상한선을 결정하는데, 이를 무작정 늘리면 리더 선출 오버헤드와 파일 핸들 고갈 문제가 뒤따른다. 또한 파티션 키 설계가 잘못되면 특정 파티션에 트래픽이 몰리는 핫스팟이 발생하고, 일부 컨슈머만 과부하 상태에 빠진다.
중복 메시지 처리(Deduplication) 역시 간과하기 쉬운 문제다. Kafka의 at-least-once 전달 보장은 네트워크 재시도나 컨슈머 리밸런싱 과정에서 동일 메시지가 두 번 이상 처리될 수 있음을 의미한다. 아래처럼 Redis를 활용한 멱등성 처리로 이를 방어할 수 있다.
String dedupKey = "event:dedup:" + event.getEventId();
Boolean isNew = redisTemplate.opsForValue()
.setIfAbsent(dedupKey, "1", Duration.ofMinutes(10));
if (Boolean.TRUE.equals(isNew)) {
processEvent(event);
}
JVM 튜닝과 컨슈머 장애의 연쇄 전파
Java 기반 시스템에서 JVM GC 튜닝은 실시간 처리 성능에 직접적인 영향을 미친다. Full GC가 발생하는 순간 컨슈머가 일시 정지(Stop-the-World)되고, Kafka 브로커는 해당 컨슈머를 죽은 것으로 판단해 리밸런싱을 트리거한다. 리밸런싱 중에는 전체 컨슈머 그룹의 처리가 멈추므로, 대규모 시스템에서는 GC 일시 정지 시간이 곧 장애 시간이 된다. G1GC 또는 ZGC로 전환하고 session.timeout.ms, max.poll.interval.ms 값을 GC 특성에 맞게 조정하는 것이 필수적이다.
컨슈머 장애의 연쇄 전파(Cascading Failure) 는 EDA의 가장 위험한 함정 중 하나다. 하나의 컨슈머가 느려지거나 장애가 나면 파티션 처리가 지연되고, 이로 인해 다운스트림 서비스들이 타임아웃을 맞으며 연쇄적으로 과부하 상태에 빠진다. 이를 막기 위해서는 각 컨슈머에 서킷 브레이커를 적용하고, 처리 실패 이벤트를 Dead Letter Topic(DLT)으로 격리해 주요 처리 파이프라인이 오염되지 않도록 설계해야 한다.
// Dead Letter Topic으로 격리
@KafkaListener(topics = "call-events")
public void consume(CallEvent event) {
try {
eventProcessor.process(event);
} catch (NonRetryableException e) {
kafkaTemplate.send("call-events.DLT", event);
}
}
정리
- 상태 관리와 중복 처리는 EDA 설계 초기부터 고려해야 하며, Redis 기반의 원자적 상태 업데이트와 멱등성 키 패턴이 실전에서 검증된 해법이다.
- JVM GC 설정과 Kafka 컨슈머 타임아웃 파라미터는 반드시 함께 튜닝해야 한다. GC 일시 정지가 곧 리밸런싱 트리거가 되기 때문이다.
- 연쇄 장애 방어를 위해 서킷 브레이커와 Dead Letter Topic을 조합하면, 일부 컨슈머 장애가 전체 파이프라인으로 번지는 것을 효과적으로 막을 수 있다.