이 글에서 얻는 것

  • Spring Events로 컴포넌트 간 느슨한 결합을 달성합니다
  • @TransactionalEventListener로 트랜잭션과 이벤트를 연동합니다
  • 비동기 이벤트와 에러 처리 패턴을 알아봅니다

왜 이벤트 기반인가?

문제: 강한 결합

// ❌ 강한 결합
@Service
public class OrderService {
    
    @Autowired
    private NotificationService notificationService;
    @Autowired
    private InventoryService inventoryService;
    @Autowired
    private PointService pointService;
    @Autowired
    private AnalyticsService analyticsService;
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = orderRepository.save(new Order(request));
        
        // 주문 외 로직이 OrderService에 누적
        notificationService.sendOrderConfirmation(order);
        inventoryService.decreaseStock(order);
        pointService.addPoints(order);
        analyticsService.trackOrder(order);
        
        return order;
    }
}

문제점:

  • OrderService가 모든 서비스에 의존
  • 새 기능 추가 시 OrderService 수정 필요
  • 테스트 어려움

해결: 이벤트 발행

flowchart LR
    OS[OrderService] -->|publish| E[OrderCreatedEvent]
    
    E --> NS[NotificationService]
    E --> IS[InventoryService]
    E --> PS[PointService]
    E --> AS[AnalyticsService]
    
    style E fill:#e3f2fd,stroke:#1565c0
// ✅ 느슨한 결합
@Service
public class OrderService {
    
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = orderRepository.save(new Order(request));
        
        // 이벤트만 발행 → 다른 서비스는 모름
        eventPublisher.publishEvent(new OrderCreatedEvent(order));
        
        return order;
    }
}

기본 이벤트 구현

이벤트 클래스

// 간단한 이벤트 (Spring 4.2+, ApplicationEvent 상속 불필요)
public class OrderCreatedEvent {
    private final Order order;
    private final LocalDateTime occurredAt;
    
    public OrderCreatedEvent(Order order) {
        this.order = order;
        this.occurredAt = LocalDateTime.now();
    }
    
    public Order getOrder() { return order; }
    public LocalDateTime getOccurredAt() { return occurredAt; }
}

이벤트 리스너

@Component
public class OrderEventListener {
    
    // 기본 동기 리스너
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        Order order = event.getOrder();
        log.info("Order created: {}", order.getId());
        
        // 알림 전송
        notificationService.sendOrderConfirmation(order);
    }
    
    // 조건부 리스너
    @EventListener(condition = "#event.order.totalAmount > 100000")
    public void handleHighValueOrder(OrderCreatedEvent event) {
        // 고액 주문만 처리
        vipService.notifyVipTeam(event.getOrder());
    }
}

@TransactionalEventListener

트랜잭션 바인딩

sequenceDiagram
    participant Service
    participant DB
    participant EventListener
    
    Service->>DB: 주문 저장
    Service->>Service: publishEvent()
    Service->>DB: 커밋
    
    alt AFTER_COMMIT (기본)
        DB-->>EventListener: 커밋 후 실행
    else AFTER_ROLLBACK
        DB--x EventListener: 롤백 시 실행
    else BEFORE_COMMIT
        Service->>EventListener: 커밋 전 실행
    end

사용 예시

@Component
public class OrderTransactionalListener {
    
    // 트랜잭션 커밋 후 실행 (기본)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderCreatedAfterCommit(OrderCreatedEvent event) {
        // DB 커밋 확정 후에만 실행
        // 외부 API 호출, 이메일 발송 등
        notificationService.sendEmail(event.getOrder());
    }
    
    // 트랜잭션 롤백 시 실행
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void onOrderFailedRollback(OrderCreatedEvent event) {
        // 보상 로직 실행
        log.error("Order failed, cleaning up: {}", event.getOrder().getId());
    }
    
    // 커밋 전 실행 (같은 트랜잭션)
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void onOrderCreatedBeforeCommit(OrderCreatedEvent event) {
        // 추가 검증 또는 같은 트랜잭션에서 처리할 로직
        auditService.logOrderCreation(event.getOrder());
    }
}

주의: 트랜잭션이 없는 경우

// ⚠️ 트랜잭션 없으면 리스너 실행 안됨
@TransactionalEventListener
public void handle(OrderCreatedEvent event) { ... }

// 해결: fallbackExecution = true
@TransactionalEventListener(fallbackExecution = true)
public void handle(OrderCreatedEvent event) {
    // 트랜잭션 없어도 실행
}

비동기 이벤트

설정

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("event-async-");
        executor.setRejectedExecutionHandler(new CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

비동기 리스너

@Component
public class AsyncOrderListener {
    
    @Async
    @EventListener
    public void handleOrderCreatedAsync(OrderCreatedEvent event) {
        // 별도 스레드에서 실행
        // 발행자는 기다리지 않음
        log.info("Processing in thread: {}", Thread.currentThread().getName());
        
        analyticsService.trackOrder(event.getOrder());
    }
    
    // 비동기 + 트랜잭션 바인딩
    @Async
    @TransactionalEventListener
    public void handleAfterCommitAsync(OrderCreatedEvent event) {
        // 커밋 후 별도 스레드에서 실행
        emailService.sendOrderConfirmation(event.getOrder());
    }
}

에러 처리

@Configuration
public class AsyncExceptionConfig implements AsyncConfigurer {
    
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, params) -> {
            log.error("Async error in {}: {}", method.getName(), throwable.getMessage());
            
            // 알림 또는 재시도 로직
            alertService.notifyError(throwable);
        };
    }
}

이벤트 체이닝

이벤트가 이벤트를 발행

@Component
public class OrderWorkflow {
    
    @Autowired
    private ApplicationEventPublisher publisher;
    
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // 재고 감소 후 새 이벤트 발행
        inventoryService.decrease(event.getOrder());
        publisher.publishEvent(new InventoryDecreasedEvent(event.getOrder()));
    }
    
    @EventListener
    public void onInventoryDecreased(InventoryDecreasedEvent event) {
        // 포인트 적립 후 새 이벤트 발행
        pointService.addPoints(event.getOrder());
        publisher.publishEvent(new PointsAddedEvent(event.getOrder()));
    }
}

순서 제어

@Component
public class OrderedEventListener {
    
    @EventListener
    @Order(1)  // 먼저 실행
    public void firstHandler(OrderCreatedEvent event) {
        log.info("First handler");
    }
    
    @EventListener
    @Order(2)  // 나중에 실행
    public void secondHandler(OrderCreatedEvent event) {
        log.info("Second handler");
    }
}

외부 메시지 시스템과 연동

Spring Events → Kafka

@Component
public class KafkaEventBridge {
    
    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void bridgeToKafka(OrderCreatedEvent event) {
        // 로컬 이벤트 → Kafka 메시지
        kafkaTemplate.send("orders", event.getOrder().getId(), 
            new OrderMessage(event.getOrder()));
    }
}

Transactional Outbox 패턴

@Entity
@Table(name = "outbox_events")
public class OutboxEvent {
    @Id
    private String id;
    private String aggregateType;
    private String aggregateId;
    private String eventType;
    private String payload;
    private LocalDateTime createdAt;
    private Boolean published;
}

@Component
public class OutboxEventListener {
    
    @Autowired
    private OutboxRepository outboxRepository;
    
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void saveToOutbox(OrderCreatedEvent event) {
        // 같은 트랜잭션에 Outbox 저장
        OutboxEvent outbox = new OutboxEvent();
        outbox.setAggregateType("Order");
        outbox.setEventType("OrderCreated");
        outbox.setPayload(objectMapper.writeValueAsString(event));
        outboxRepository.save(outbox);
    }
}

// 별도 스케줄러가 Outbox 폴링 → Kafka 발행

요약

이벤트 유형 선택

요구사항방법
동기 처리@EventListener
커밋 후 처리@TransactionalEventListener
비동기 처리@Async + @EventListener
순서 제어@Order
조건부 처리condition 속성

핵심 원칙

  1. 느슨한 결합: 발행자는 구독자를 모름
  2. 단일 책임: 각 리스너는 하나의 역할
  3. 트랜잭션 인지: 커밋/롤백에 따른 처리
  4. 에러 격리: 비동기로 실패 전파 방지