Spring Boot - EventListener!

EventListener

의존성 분리를 위해 Spring 에서 지원하는 이벤트 처리 방식
아래와 같이 @EventListener 어노테이션으로 이벤트를 처리할 메서드를 정의해 놓으면, 이벤트가 처리될 위치에서 AOP 를 통해 EventListener 핸들러 함수가 호출되도록 설정된다.

@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 함수는 ApplicationContextoverridepublishEvent 로 흘러들어가게되고, 정의한 EventListener 핸들러 함수 를 호출하게 된다.

@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);
        // 해당 부분에서 aop 를 통해 handleAccountCreateEvent 메서드가 호출되게됨
        eventPublisher.publishEvent(new AccountCreateEvent(result));
        // if (new Random().nextInt() % 2 == 0)
        //     throw new IllegalArgumentException("temp exception");
        log.info("account return");
        return result;
    }
}

아무리 비동기로 이벤트 처리 스레드를 나누었다 하더라도 theradPoolqueueCapacity 를 넘어가는 이벤트가 발생하게되면 결국 RejectedExecutionException 이 발생하게된다.

적절한 백프레셔 제한처리나 모니터링/알림 처리를 진행해야한다.

TransactionalEventListener

위의 경우 의존성은 분리했지만 트랜잭션의 일관성처리는 되고있지 않다.
이벤트는 발행했는데 후처리 과정에서 Account 저장에 실패하게 될 경우 이벤트의 발행 자체가 일관성을 망가뜨리는 행위가 된다.

때문에 트랜잭션과 이벤트를 묶어 발행시킬 수 있는 TransactionalEventListener 를 제공한다.

  • AFTER_COMMIT:트랜잭션이 성공적으로 커밋된 후에 호출.
  • AFTER_ROLLBACK: 트랜잭션이 롤백된 후에 호출.
  • AFTER_COMPLETION: 트랜잭션이 커밋되었는지 롤백되었는지에 관계없이 트랜잭션이 완료된 후에 호출.
  • BEFORE_COMMIT: 트랜잭션이 커밋되기 직전에 호출.
@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 메서드로 인해 바로 호출되지 않음

[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

카테고리:

업데이트: