EventListener 의존성 분리를 위해 Spring 에서 지원하는 이벤트 처리 방식 아래와 같이 @EventListener 어노테이션으로 이벤트를 처리할 메서드를 정의해 놓으면, 이벤트가 처리될 위치에서 AOP 를 통해 EventListener 핸들러 함수가 호출되도록 설정된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Bean(name = "taskExecutor") public Executor taskExecutor () { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor (); executor.setCorePoolSize(10 ); executor.setMaxPoolSize(50 ); executor.setQueueCapacity(100 ); executor.setThreadNamePrefix("AsyncThread-" ); executor.setRejectedExecutionHandler(new ThreadPoolExecutor .CallerRunsPolicy()); executor.initialize(); return executor; } ... @Component public class DemoEventListener { @Async("taskExecutor") @EventListener public void handleAccountCreateEvent (AccountCreateEvent event) throws InterruptedException { Thread.sleep(2000 ); System.out.println("Received custom event, account:" + event.getAccount()); } }
AOP 로 호출하게 되면 당연히 하나의 스레드로 처리되기 때문에 좀더 확실한 의존성 분리를 위해 @Async 어노테이션을 사용하는 경우가 많다.
아래와 같이 publishEvent 함수를 호출하게 되면 내부에서 정의한 EventListener 핸들러 함수 를 호출하게 된다.
publishEvent 함수는 ApplicationContext 에 override 된 publishEvent 로 흘러들어가게되고, 정의한 EventListener 핸들러 함수 를 호출하게 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Slf4j @Service @RequiredArgsConstructor public class AccountService { private final AccountRepository repository; private final ApplicationEventPublisher eventPublisher; @Transactional public AccountDto createAccount (CreateAccountRequestDto dto) { log.info("createAccount invoked" ); AccountEntity entity = toEntity(dto); entity = repository.save(entity); log.info("account save end" ); AccountDto result = toDto(entity); eventPublisher.publishEvent(new AccountCreateEvent (result)); log.info("account return" ); return result; } }
아무리 비동기로 이벤트 처리 스레드를 나누었다 하더라도 theradPool 의 queueCapacity 를 넘어가는 이벤트가 발생하게되면 결국 RejectedExecutionException 이 발생하게된다.
적절한 백프레셔 제한처리나 모니터링/알림 처리를 진행해야한다.
TransactionalEventListener 위의 경우 의존성은 분리했지만 트랜잭션의 일관성처리는 되고있지 않다. 이벤트는 발행했는데 후처리 과정에서 Account 저장에 실패하게 될 경우 이벤트의 발행 자체가 일관성을 망가뜨리는 행위가 된다.
때문에 트랜잭션과 이벤트를 묶어 발행시킬 수 있는 TransactionalEventListener 를 제공한다.
AFTER_COMMIT:트랜잭션이 성공적으로 커밋된 후에 호출.
AFTER_ROLLBACK: 트랜잭션이 롤백된 후에 호출.
AFTER_COMPLETION: 트랜잭션이 커밋되었는지 롤백되었는지에 관계없이 트랜잭션이 완료된 후에 호출.
BEFORE_COMMIT: 트랜잭션이 커밋되기 직전에 호출.
1 2 3 4 5 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleAccountCreateEvent (AccountCreateEvent event) throws InterruptedException { Thread.sleep(2000 ); log.info("Received custom event, account:" + event.getAccount()); }
실제 출력된 결과를 보면 account return 로그가 출력된 뒤 EventListener 함수가 호출되었다.
Async 메세지를 제거해도 publishEvent 메서드로 인해 바로 호출되지 않음
1 2 3 4 [nio-8080-exec-1] c.e.e.service.AccountService : createAccount invoked [nio-8080-exec-1] c.e.e.service.AccountService : account save end [nio-8080-exec-1] c.e.e.service.AccountService : account return [nio-8080-exec-1] c.e.e.event.DemoEventListener : Received custom event, account:AccountDto(accountId=1, name=kouzie, username=kouzie, email=kouzie@naver.com)
내부적으로 publishEvent 호출시 EventListener 핸들러 함수 가 callback 함수 형태로 동작하도록 저장해놓고, 나중에 트랜잭션이 완료된 후 해당 callback 함수가 동작되도록 하는 방식이다.
org.springframework.transaction 라이브러리에서 callback 함수 등록을 지원하기 때문에 가능한 설정이다.
publishEvent 가 호출될 떄 마다 TransactionalApplicationListenerSynchronization 인스턴스를 계속 생성하고 TransactionSynchronizationManager 에 리스너(callback 함수)로 등록한다.
데모코드
https://github.com/Kouzie/spring-boot-demo/tree/main/event-listener-demo