-
이벤트 기반 아키텍처 구축기: 주문부터 데이터 플랫폼까지Develop 2025. 8. 29. 14:20
서론
기존의 동기식 처리로는 다음과 같은 문제점들이 발생했습니다.
- 🐌 성능 저하: 외부 시스템 호출로 인한 응답 지연
- 🔗 강한 결합: 도메인 간 의존성 증가
- 🚫 장애 전파: 하나의 실패가 전체 플로우에 영향
- 📈 확장성 제한: 새로운 요구사항 추가 시 기존 코드 수정 필요
해결책: 이벤트 기반 아키텍처 도입
1. 기본 이벤트 시스템 설계
EventEnvelope 패턴 (봉투 패턴)
모든 이벤트를 감싸는 메타 데이터 컨테이너를 구현했습니다.
@Getter public class EventEnvelope<T extends Event> { private final String eventId; // 이벤트 고유 ID private final LocalDateTime occurredAt; // 발생 시간 private final String eventType; // 이벤트 타입 private final T payload; // 실제 이벤트 데이터 private final String correlationId; // 연관 추적 ID private final String aggregateId; // 애그리게이트 ID public EventEnvelope(T payload, String correlationId) { this.eventId = UUID.randomUUID().toString(); this.occurredAt = payload.getOccurredAt(); this.eventType = payload.getEventType(); this.payload = payload; this.correlationId = correlationId; this.aggregateId = payload.getAggregateId(); } }봉투 패턴의 장점:
- 🏷️ 이벤트 메타데이터 표준화
- 🔍 추적 가능한 이벤트 ID 및 상관관계 ID
- 📊 감사 로그 및 모니터링 용이성
Spring Integration을 위한 DomainApplicationEvent
@Getter public class DomainApplicationEvent extends ApplicationEvent { private final EventEnvelope<?> envelope; public DomainApplicationEvent(Object source, EventEnvelope<?> envelope) { super(source); this.envelope = envelope; } @SuppressWarnings("unchecked") public <T> T getPayload(Class<T> type) { Object payload = envelope.getPayload(); if (type.isInstance(payload)) { return (T) payload; } throw new CoreException(ErrorType.NOT_FOUND, "페이로드 타입이 맞지 않습니다.: " + type.getName()); } }통합된 EventPublisher
@Component @RequiredArgsConstructor public class EventPublisher { private final ApplicationEventPublisher springEventPublisher; public void publish(Event event) { EventEnvelope<Event> envelope = EventEnvelope.of(event); DomainApplicationEvent domainApplicationEvent = new DomainApplicationEvent(this, envelope); try { springEventPublisher.publishEvent(domainApplicationEvent); } catch (Exception e) { log.error("이벤트 발행 실패: {} [{}]", envelope.getEventType(), envelope.getEventId(), e); throw e; } } }2. 주문 → 결제 → 데이터 플랫폼 플로우 구현
주문 생성 이벤트 처리
@Component @RequiredArgsConstructor public class OrderEventHandler { private final EventPublisher eventPublisher; private final ObjectMapper objectMapper; @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void processOrderCreated(OrderCreatedEvent event) { // 1. 데이터 플랫폼으로 주문 데이터 전송 sendOrderDataToPlatform(event); // 2. 결제 요청 if (event.paymentMetadata() != null) { PaymentRequestEvent paymentRequestEvent = PaymentRequestEvent.create( event.orderId(), event.userId(), event.totalAmount(), event.paymentMetadata().cardType(), event.paymentMetadata().cardNo(), event.paymentMetadata().callbackUrl() ); eventPublisher.publish(paymentRequestEvent); } } private void sendOrderDataToPlatform(OrderCreatedEvent event) { try { String orderData = objectMapper.writeValueAsString(event); DataPlatformEvent dataPlatformEvent = DataPlatformEvent.fromOrder( event.orderId(), orderData ); eventPublisher.publish(dataPlatformEvent); } catch (JsonProcessingException e) { log.error("주문 데이터 JSON 변환 실패: {}", event.orderId(), e); } } }결제 처리 및 후속 작업
@Component @RequiredArgsConstructor public class PaymentEventHandler { private final PaymentProcessor paymentProcessor; private final EventPublisher eventPublisher; @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void processPaymentRequest(PaymentRequestEvent event) { try { // 실제 결제 처리 PaymentInfo paymentInfo = paymentProcessor.process( new PaymentCommand.CreatePayment( event.userId(), event.orderId(), event.cardType(), event.cardNo(), event.amount(), event.callbackUrl() ) ); // 결제 성공 이벤트 발행 PaymentCompletedEvent completedEvent = PaymentCompletedEvent.create( event.orderId(), event.userId(), paymentInfo.transactionKey() ); eventPublisher.publish(completedEvent); sendPaymentDataToPlatform(completedEvent); } catch (Exception e) { // 결제 실패 이벤트 발행 PaymentFailedEvent failedEvent = PaymentFailedEvent.create( event.orderId(), event.userId(), null, event.amount(), e ); eventPublisher.publish(failedEvent); sendPaymentDataToPlatform(failedEvent); } } }데이터 플랫폼 연동 처리
@Component @RequiredArgsConstructor public class DataPlatformEventHandler { private final DataPlatformService dataPlatformService; @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void processDataPlatformSync(DataPlatformEvent event) { try { dataPlatformService.sendData( event.dataType(), event.aggregateId(), event.payload(), event.source() ); log.info("데이터 플랫폼 전송 완료: {} - {}", event.dataType(), event.aggregateId()); } catch (Exception e) { // 재시도 로직 log.error("데이터 플랫폼 전송 실패: {} - {}", event.dataType(), event.aggregateId(), e); } } }3. 이벤트 감사 시스템 구축
모든 이벤트를 추적하고 감사하기 위한 시스템을 구축했습니다:
@Component @RequiredArgsConstructor public class EventAuditHandler { private final EventAuditLogRepository eventAuditLogRepository; @EventListener @Order(Ordered.HIGHEST_PRECEDENCE) // 최우선 순위로 실행 @Transactional(propagation = Propagation.REQUIRES_NEW) // 독립적인 트랜잭션 public void auditAllEvents(DomainApplicationEvent event) { log.info("=== 이벤트 감사 ==="); log.info("ID: {}", event.getEventId()); log.info("타입: {}", event.getEventType()); log.info("애그리게이트 ID: {}", event.getAggregateId()); log.info("코릴레이션 ID: {}", event.getCorrelationId()); log.info("발생시간: {}", event.getOccurredAt()); log.info("=================="); EventAuditLog auditLog = EventAuditLog.from(event); eventAuditLogRepository.save(auditLog); } }4. 구현 결과 및 효과
- 시스템 안정성 확보
- 장애 격리: 데이터 플랫폼 장애가 주문/결제에 영향 없음
- 복구 가능성: 이벤트 로그를 통한 재처리 지원
- 모니터링: 각 이벤트 단계별 추적 가능
- 개발 생산성 향상
- 모듈화: 각 도메인의 독립적 개발 가능
- 확장성: 새로운 이벤트 핸들러 추가만으로 기능 확장
- 테스트 용이성: 각 컴포넌트별 단위 테스트 가능
- 코드 품질 개선
- 단일 책임 원칙: 각 핸들러가 명확한 책임 보유
- 낮은 결합도: 도메인 간 직접 의존성 제거
- 높은 응집도: 관련 기능들의 응집성 향상
마무리
이벤트 기반 아키텍처 도입을 통해 우리는 단순히 기술적인 성능 향상뿐만 아니라, 비즈니스 요구사항에 빠르게 대응할 수 있는 유연한 시스템을 구축할 수 있었습니다.
특히 봉투 패턴(EventEnvelope)을 통한 이벤트 표준화와 감사 시스템을 통한 추적 가능성 확보 경험할 수 있었습니다.