
Event-Driven Architecture(EDA)는 시스템 컴포넌트들이 이벤트를 통해 느슨하게 결합되는 설계 패턴입니다. 서비스 간 직접 호출 대신 이벤트를 발행(Publish)하고 구독(Subscribe)하는 구조로, 높은 확장성과 탄력성을 확보할 수 있습니다. 이 글에서는 EDA의 핵심 개념부터 Spring Boot와 Apache Kafka를 활용한 실제 적용법까지 단계별로 살펴봅니다.
Event-Driven Architecture의 핵심 구성 요소
EDA는 세 가지 핵심 역할로 구성됩니다.
- Producer (이벤트 발행자): 특정 도메인 행위가 완료되면 이벤트를 생성해 메시지 브로커로 전달합니다.
- Broker (메시지 브로커): 이벤트를 수신·저장하고 Consumer에게 전달합니다. Apache Kafka, RabbitMQ, AWS SNS/SQS 등이 대표적입니다.
- Consumer (이벤트 구독자): 관심 있는 이벤트 토픽을 구독하고 비동기로 처리합니다.
이 구조에서 Producer는 누가 이벤트를 소비하는지 알 필요가 없습니다. 이것이 기존 동기 방식의 REST 호출과 가장 큰 차이점이며, 서비스 간 의존성 제거의 핵심입니다.
동기 호출 vs. 이벤트 기반 설계 비교
변경전 — 서비스 간 직접 HTTP 호출
// OrderService.java — 직접 호출 방식 (강결합)
@Service
public class OrderService {
private final InventoryClient inventoryClient;
private final NotificationClient notificationClient;
public void placeOrder(Order order) {
// 재고 차감 — 실패 시 전체 트랜잭션 롤백 필요
inventoryClient.deduct(order.getProductId(), order.getQuantity());
// 알림 전송 — 응답 대기 시간이 주문 완료 속도에 직접 영향
notificationClient.sendConfirmation(order.getUserId());
}
}변경후 — 이벤트 발행 방식 (느슨한 결합)
// OrderService.java — 이벤트 발행 방식
@Service
@RequiredArgsConstructor
public class OrderService {
private final KafkaTemplate<String, OrderPlacedEvent> kafkaTemplate;
public void placeOrder(Order order) {
// 주문 저장 후 이벤트만 발행; Consumer는 독립적으로 처리
orderRepository.save(order);
OrderPlacedEvent event = new OrderPlacedEvent(
order.getId(), order.getProductId(), order.getQuantity(), order.getUserId()
);
kafkaTemplate.send("order.placed", event); // 비동기 발행
}
}변경 후에는 InventoryService와 NotificationService가 각자의 속도로 이벤트를 소비합니다. 주문 서비스는 두 서비스의 응답을 기다리지 않아 응답 속도가 크게 향상됩니다.
Kafka Consumer 구현 및 멱등성 처리
Consumer를 구현할 때 가장 중요한 설계 원칙은 멱등성(Idempotency)입니다. 네트워크 장애나 재시도로 인해 동일 이벤트가 두 번 이상 전달될 수 있기 때문입니다.
아래는 재고 차감 Consumer의 멱등성 처리 예시입니다.
// InventoryConsumer.java
@Component
@RequiredArgsConstructor
public class InventoryConsumer {
private final InventoryRepository inventoryRepository;
private final ProcessedEventRepository processedEventRepository;
@KafkaListener(topics = "order.placed", groupId = "inventory-service")
public void handleOrderPlaced(OrderPlacedEvent event,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
// 이미 처리된 이벤트인지 확인 (멱등성 보장)
if (processedEventRepository.existsByEventId(event.getOrderId())) {
log.info("중복 이벤트 무시: orderId={}", event.getOrderId());
return;
}
// 재고 차감 처리
inventoryRepository.deduct(event.getProductId(), event.getQuantity());
// 처리 완료 기록
processedEventRepository.save(new ProcessedEvent(event.getOrderId(), topic));
log.info("재고 차감 완료: productId={}, qty={}", event.getProductId(), event.getQuantity());
}
}// 콘솔 출력 예시
재고 차감 완료: productId=PROD-001, qty=2
중복 이벤트 무시: orderId=ORD-9921 ← 재시도 이벤트 안전하게 처리processedEventRepository는 Redis나 RDB를 활용할 수 있습니다. Redis를 사용하면 TTL을 설정해 오래된 이벤트 ID를 자동으로 만료시킬 수 있어 메모리 효율도 확보됩니다.
Dead Letter Queue로 장애 복원력 확보
EDA에서 Consumer가 이벤트 처리에 실패할 경우 이벤트를 유실하지 않도록 Dead Letter Queue(DLQ) 패턴을 적용합니다. Spring Kafka에서는 @RetryableTopic 어노테이션으로 재시도와 DLQ를 선언적으로 설정할 수 있습니다.
// NotificationConsumer.java — 재시도 및 DLQ 설정
@Component
public class NotificationConsumer {
@RetryableTopic(
attempts = "3", // 최대 3회 재시도
backoff = @Backoff(delay = 1000, multiplier = 2.0), // 지수 백오프
dltTopicSuffix = ".dlt" // 실패 시 order.placed.dlt 토픽으로 이동
)
@KafkaListener(topics = "order.placed", groupId = "notification-service")
public void handleOrderPlaced(OrderPlacedEvent event) {
notificationService.sendEmail(event.getUserId(), event.getOrderId());
}
// DLQ 전용 Consumer — 알림, 수동 재처리 등 후속 조치
@DltHandler
public void handleDlt(OrderPlacedEvent event) {
log.error("이벤트 최종 처리 실패, DLQ 기록: orderId={}", event.getOrderId());
alertOpsTeam(event); // 운영팀 알림
}
}DLQ 패턴을 적용하면 일시적인 외부 서비스 장애가 전체 이벤트 파이프라인을 멈추지 않습니다. 장애 복원력을 높이는 핵심 아키텍처 패턴입니다.
맺음말
Event-Driven Architecture는 서비스 간 직접 의존성을 제거하고 각 컴포넌트가 독립적으로 확장될 수 있는 구조를 만들어 줍니다. 핵심 설계 원칙을 정리하면 다음과 같습니다.
- 이벤트는 과거형으로 명명하세요:
OrderPlaced,PaymentCompleted처럼 이미 발생한 사실을 표현해야 Consumer가 의미를 명확히 이해합니다. - 멱등성은 선택이 아닌 필수입니다: 네트워크 재시도는 언제든 발생하며, Consumer는 항상 중복 이벤트를 처리할 준비가 되어 있어야 합니다.
- DLQ로 이벤트 유실을 방지하세요: 단순 로그로 끝내지 않고 반드시 재처리 경로를 설계해야 합니다.
EDA가 적합한 상황은 서비스 간 결합도를 낮춰야 할 때, 트래픽 급증에 대비한 확장성이 필요할 때, 또는 여러 서비스가 동일 이벤트에 반응해야 할 때입니다. 반면 트랜잭션 일관성이 엄격하게 요구되는 소규모 모놀리식 환경에서는 오히려 복잡도가 증가할 수 있습니다.
관련 심화 주제로 Saga 패턴을 통한 분산 트랜잭션 처리, Event Sourcing을 통한 상태 이력 관리를 함께 학습하면 EDA 기반 아키텍처를 더욱 견고하게 설계할 수 있습니다.
참고 자료
'프로그래밍 PROGRAMMING > 아키텍쳐' 카테고리의 다른 글
| Transactional Outbox 패턴으로 분산 시스템 이벤트 신뢰성 보장하기 (0) | 2026.04.03 |
|---|---|
| [SPRING/JAVA] Spring Boot 멀티모듈 프로젝트 구조 설계하기 (0) | 2026.04.02 |
| DDD Aggregate 경계 설계 전략 (0) | 2026.03.30 |
| Outbox 패턴으로 분산 시스템 이벤트 유실 없이 발행하기 (0) | 2026.03.25 |
| Event Sourcing 아키텍처 패턴 적용하기 (0) | 2026.03.23 |