Taehwan Dev Blog

쿠버네티스, eBPF, 그리고 머신러닝 이야기

View My GitHub Profile

8 April 2026

Event-driven 아키텍처로 구현한 DDD 모듈러 모놀리스 애플리케이션

by Yang Taehwan

프로젝트 소개

깃헙 레포지토리

개발 스택

구현 상세

Payment

@Service
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final ApplicationEventPublisher applicationEventPublisher;

    // ...

    @Transactional
    public void approvePayment(Long paymentId) {
        Payment payment = paymentRepository.findById(paymentId)
                .orElseThrow(() -> new IllegalArgumentException("Payment does not exist. paymentId=" + paymentId));

        payment.approve();

        PaymentCompletedEvent event = new PaymentCompletedEvent(
                UUID.randomUUID().toString(),
                payment.getId(),
                payment.getOrderId(),
                payment.getUserId(),
                payment.getAmount()
        );

        applicationEventPublisher.publishEvent(event);
    }
}

Order

@Component
public class OrderPaymentEventHandler {
    // ...
    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handle(PaymentCompletedEvent event) {
        try {
            Order order = orderRepository.findById(event.orderId())
                    .orElseThrow(() -> new IllegalArgumentException(
                            "Order does not exist. orderId=" + event.orderId()
                    ));

            order.markPaid();

            OrderPaidEvent orderPaidEvent = new OrderPaidEvent(
                    UUID.randomUUID().toString(),
                    order.getId(),
                    order.getUserId()
            );

            String payload = objectMapper.writeValueAsString(orderPaidEvent);

            // OutboxEvent를 만든 후
            OutboxEvent outboxEvent = OutboxEvent.pending(
                    orderPaidEvent.eventId(),
                    "ORDER",
                    String.valueOf(order.getId()),
                    "OrderPaidEvent",
                    payload
            );
            // 데이터베이스에 저장한다.
            // 이후 이벤트를 모든 consumer가 소비하면 이벤트의 상태를 변경한다.(PENDING -> SENT)
            outboxRepository.save(outboxEvent);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("OrderPaidEvent serialization failed", e);
        } catch (Exception e) {
            log.error("Failed to handle PaymentCompletedEvent. paymentId={}, orderId={}",
                    event.paymentId(), event.orderId(), e);
            throw e;
        }
    }
}

Outbox

@Component
public class OutboxPublisher {
    // ...

    // 3000ms 주기
    // publish는 @Transactional이 아니다.
    // PENDING 상태의 이벤트 목록을 가져와 각 이벤트를 트랜잭션 처리 함수롤 전달한다.
    @Scheduled(fixedDelay = 3000)
    public void publish() {
        List<OutboxEvent> events =
                outboxRepository.findTop100ByStatusOrderByIdAsc(OutboxStatus.PENDING);

        for (OutboxEvent event : events) {
            publishSingleEvent(event.getId());
        }
    }

    // Event를 하나씩 가져와 OutboxEventDispatcher에 이벤트 타입에 따른 처리를 위임한다.
    @Transactional
    public void publishSingleEvent(Long outboxEventId) {
        OutboxEvent event = outboxRepository.findById(outboxEventId)
                .orElseThrow(() -> new IllegalArgumentException(
                        "Outbox event does not exist. outboxEventId=" + outboxEventId
                ));

        if (event.getStatus() != OutboxStatus.PENDING) {
            log.info("Skipping outbox event because status is not PENDING. eventId={}, status={}",
                    event.getEventId(),
                    event.getStatus()
            );
            return;
        }

        try {
            log.info("Publishing outbox event. eventId={}, eventType={}, aggregateType={}, aggregateId={}",
                    event.getEventId(),
                    event.getEventType(),
                    event.getAggregateType(),
                    event.getAggregateId()
            );

            outboxEventDispatcher.dispatch(event);
            event.markSent();

            log.info("Outbox event published successfully. eventId={}", event.getEventId());
        } catch (Exception e) {
            log.error("Failed to publish outbox event. eventId={}", event.getEventId(), e);
            event.markFailed();
        }
    }
}
@Component
public class OutboxEventDispatcher {
    // ...

    // 이벤트 타입에 따라 이벤트를 처리하는 핸들러로 분기 처리한다.
    @Transactional
    public void dispatch(OutboxEvent outboxEvent) {
        if ("OrderPaidEvent".equals(outboxEvent.getEventType())) {
            handleOrderPaidEvent(outboxEvent);
            return;
        }

        throw new IllegalArgumentException("Unsupported event type. eventType=" + outboxEvent.getEventType());
    }

    // OrderPaidEvent 이벤트 핸들러 함수
    private void handleOrderPaidEvent(OutboxEvent outboxEvent) {
        try {
            OrderPaidEvent event = objectMapper.readValue(
                    outboxEvent.getPayload(),
                    OrderPaidEvent.class
            );

            deliveryService.createDelivery(event);
            notificationService.sendOrderPaidNotification(event);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("OrderPaidEvent deserialization failed", e);
        }
    }
}
tags: