개요
Spring Framework의 이벤트 시스템은 애플리케이션 내부 컴포넌트 간 느슨한 결합을 가능하게 하는 강력한 메커니즘입니다. ApplicationContext는 이벤트 발행과 리스너 등록을 담당하며, Spring 4.2부터 도입된 @EventListener 어노테이션으로 복잡한 설정 없이 간결하게 리스너를 정의할 수 있습니다.
이벤트 기반 프로그래밍을 구현하려면 세 가지 핵심 요소가 필요합니다:
- ApplicationEvent를 상속한 커스텀 이벤트 정의
- ApplicationEventPublisher를 주입받은 이벤트 발행자
- ApplicationListener 인터페이스 또는
@EventListener로 구현된 리스너
기본 구현
커스텀 이벤트 클래스
비즈니스 상황에 맞는 이벤트 객체를 설계합니다. payload 데이터와 발생 시점 정보를 함께 전달할 수 있습니다.
public class OrderCompletedEvent extends ApplicationEvent {
private final Long orderId;
private final BigDecimal totalAmount;
private final LocalDateTime occurredAt;
public OrderCompletedEvent(Object source, Long orderId, BigDecimal totalAmount) {
super(source);
this.orderId = orderId;
this.totalAmount = totalAmount;
this.occurredAt = LocalDateTime.now();
}
public Long getOrderId() { return orderId; }
public BigDecimal getTotalAmount() { return totalAmount; }
public LocalDateTime getOccurredAt() { return occurredAt; }
}
이벤트 발행 구성
의존성 주입을 통해 ApplicationEventPublisher를 획득하고, 도메인 로직 완료 후 이벤트를 발행합니다.
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
private final OrderRepository orderRepository;
public OrderService(ApplicationEventPublisher eventPublisher,
OrderRepository orderRepository) {
this.eventPublisher = eventPublisher;
this.orderRepository = orderRepository;
}
@Transactional
public void completeOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.complete();
orderRepository.save(order);
eventPublisher.publishEvent(
new OrderCompletedEvent(this, orderId, order.calculateTotal())
);
}
}
이벤트 리스너 구현
두 가지 방식으로 리스너를 작성할 수 있습니다. 어노테이션 방식이 더 유연하고 현대적인 접근법입니다.
인터페이스 구현 방식:
@Component
public class OrderNotificationHandler implements ApplicationListener<OrderCompletedEvent> {
@Override
public void onApplicationEvent(OrderCompletedEvent event) {
notificationService.sendOrderConfirmation(event.getOrderId());
}
}
어노테이션 기반 방식:
@Component
public class OrderAnalyticsHandler {
@EventListener
public void recordOrderMetrics(OrderCompletedEvent event) {
metricsCollector.incrementOrderCounter(
event.getTotalAmount(),
event.getOccurredAt()
);
}
}
고급 활용
비동기 처리
기본적으로 리스너는 발행자와 동일한 스레드에서 실행되어 메인 흐름을 차단합니다. @EnableAsync와 @Async를 조합하여 비동기 처리를 활성화합니다.
@Configuration
@EnableAsync
public class AsyncEventConfiguration {
@Bean
public Executor eventTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("event-handler-");
executor.initialize();
return executor;
}
}
@Component
public class EmailNotificationHandler {
@Async("eventTaskExecutor")
@EventListener
public void sendEmail(OrderCompletedEvent event) {
emailService.sendCustomerNotification(event.getOrderId());
}
}
트랜잭션 연계 이벤트
트랜잭션 커밋 후에만 이벤트가 처리되도록 보장해야 하는 경우, @TransactionalEventListener를 사용합니다. 이는 트랜잭션 롤백 시 불필요한 부수 효과를 방지합니다.
@Service
public class InventoryService {
private final ApplicationEventPublisher publisher;
@Transactional
public void reserveStock(Long productId, int quantity) {
// 재고 예약 로직...
// 트랜잭션 커밋 시점에 실제 발행됨
publisher.publishEvent(new StockReservedEvent(this, productId, quantity));
}
}
@Component
public class ShippingPreparationHandler {
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT,
fallbackExecution = false
)
public void prepareShipment(StockReservedEvent event) {
// 트랜잭션 성공 후에만 실행
shippingService.createPendingShipment(event.getProductId(), event.getQuantity());
}
}
phase 옵션으로 실행 시점을 세밀하게 제어할 수 있습니다:
AFTER_COMMIT: 커밋 성공 후 (가장 일반적)BEFORE_COMMIT: 커밋 직전AFTER_ROLLBACK: 롤백 후AFTER_COMPLETION: 완료 후 (성공/실패 무관)
설계 고려사항
동기 실행이 기본값이므로, 리스너 처리 시간이 길어질 경우 메인 트랜잭션에 영향을 줍니다. 핵심 비즈니스 흐름과 분리할 수 있는 작업은 반드시 비동기로 전환하세요.
Spring Event는 애플리케이션 내부 통신에 적합하며, 메시지 전달 보장이 필요한 경우 외부 메시징 시스템(Kafka, RabbitMQ 등)을 고려해야 합니다. 이벤트 리스너는 가벼운 조정자 역할로 유지하고, 실제 비즈니스 로직은 별도 서비스로 위임하는 것이 유지보수에 유리합니다.