ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이벤트 기반 아키텍처 구축기: 주문부터 데이터 플랫폼까지
    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. 구현 결과 및 효과

    1. 시스템 안정성 확보
    • 장애 격리: 데이터 플랫폼 장애가 주문/결제에 영향 없음
    • 복구 가능성: 이벤트 로그를 통한 재처리 지원
    • 모니터링: 각 이벤트 단계별 추적 가능
    1. 개발 생산성 향상
    • 모듈화: 각 도메인의 독립적 개발 가능
    • 확장성: 새로운 이벤트 핸들러 추가만으로 기능 확장
    • 테스트 용이성: 각 컴포넌트별 단위 테스트 가능
    1. 코드 품질 개선
    • 단일 책임 원칙: 각 핸들러가 명확한 책임 보유
    • 낮은 결합도: 도메인 간 직접 의존성 제거
    • 높은 응집도: 관련 기능들의 응집성 향상

     

    마무리

    이벤트 기반 아키텍처 도입을 통해 우리는 단순히 기술적인 성능 향상뿐만 아니라, 비즈니스 요구사항에 빠르게 대응할 수 있는 유연한 시스템을 구축할 수 있었습니다.

    특히 봉투 패턴(EventEnvelope)을 통한 이벤트 표준화와 감사 시스템을 통한 추적 가능성 확보 경험할 수 있었습니다.

    댓글

Designed by Tistory.