kimyu0218
  • [spring] 스프링 이벤트 (ApplicationEvent와 ApplicationListener/@EventListener와 @TransactionalEventListener/비동기 이벤트)
    2024년 11월 15일 12시 00분 58초에 업로드 된 글입니다.
    작성자: @kimyu0218

    스프링 이벤트

    public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
    		MessageSource, ApplicationEventPublisher, ResourcePatternResolver { ... }

    스프링의 핵심인 `ApplicationContext`는 `ApplicationEventPublisher`를 상속 받아 이벤트를 게시(publish)하고 구독(listen)할 수 있도록 지원한다. 스프링 이벤트는 RabbitMQ 같은 외부 메시지 브로커와 달리 어플리케이션 내부에서 게시/구독할 목적으로 사용한다.

     

    스프링 이벤트는 다음 장점을 가진다.

    • 이벤트를 게시하는 클래스와 이벤트를 구독하는 클래스의 의존 관계를 분리할 수 있다.
    • 이벤트를 비동기로 처리할 수 있다.
    • 하나의 이벤트를 여러 개의 클래스가 구독할 수 있다.
    • 스프링 이벤트로 트랜잭션을 효율적으로 사용할 수 있다.

    `MemberService`가 `PushNotificationService`에 의존한다고 가정해보자. 두 서비스는 서로 다른 도메인에도 불구하고 `MemberService` 에서 `PushNotificationService`를 주입받는 구조일 것이다. 즉, 느슨하게 결합되어 있지만 관련 없는 두 기능이 같은 파일에 존재한다. 하지만 스프링 이벤트를 이용하면 이런 의존 관계를 해결할 수 있다.

     

    이벤트를 게시하고 구독하기 위해서는 세 가지 클래스가 필요하다.

    1. 이벤트 메시지 : 게시 클래스와 구독 클래스 사이에 공유할 데이터를 포함하는 클래스
    2. 게시 클래스 (= publisher) : 구독 클래스에 전달할 이벤트 메시지를 생성/게시하는 클래스
    3. 구독 클래스 (= listener) : `ApplicationContext`에서 전달받은 이벤트 메시지를 구독하고 부가 기능을 수행하는 클래스
    💡 이벤트는 일종의 DTO로서 게시 클래스 - `ApplicationContext` - 구독 클래스 간에 전달된다.

    지금부터 다양한 방법으로 스프링 이벤트를 구현해보자.

     

    ApplicationEvent와 ApplicationListener을 활용한 방법

    기존에는 스프링 이벤트를 사용하기 위해 `ApplicationEvent`와 `ApplicationListener`를 사용했다. 이벤트 메시지는 `ApplicationEvent`를 상속받고, 구독 클래스는 `ApplicationListener`를 구현했다.

    public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
        void onApplicationEvent(E event);
        ...
    }

    위 코드는 `ApplicationListener` 인터페이스다. `ApplicationContext`는 이벤트 메시지 타입을 확인하고 적절한 구독 클래스를 찾아 `onApplicationEvent()`를 실행한다. 따라서 구독 클래스는 `onApplicationEvent()`를 오버라이딩하여 이벤트를 처리해야 한다.

    // 이벤트 메시지
    public class CreateMemberEvent extends ApplicationEvent { ... }
    
    // 게시 클래스
    @RequiredArgsConstructor
    @Service
    public class MemberService {
        private final ApplicationEventPublisher applicationEventPublisher;
        ...
        void createMember(...) {
            ...
            applicationEventPublisher.publishEvent(createMemberEvent);
        }
    }
    
    // 구독 클래스
    @Service
    public class PushNotificationService extends ApplicationListener<CreateMemberEvent> {
        ...
        public void onApplicationEvent(final CreateMemberEvent event) { ... }
    }

    위와 같이 이벤트 메시지, 게시 클래스, 구독 클래스를 구현하면 더 이상 `MemberService`가 `PushNotificationService`를 의존하지 않아도 된다.

     

    @EventListener와 @TransactionalEventListener를 활용한 방법

    앞에서는 `ApplicationListener`를 구현했지만 스프링 4.2부터는 `@EventListener` 어노테이션을 이용할 수 있다. 이벤트 메시지 클래스는 더 이상 `ApplicationEvent`를 상속하지 않아도 되며, 구독 클래스도 이벤트를 처리하는 메서드 앞에 `@EventListener`를 붙여주기만 하면 된다.

     

    `@TransactionalEventListener`는 트랜잭션 종료 단계와 연계하여 이벤트를 구독하는 기능을 제공한다. 종료 단계는 다음과 같다.

    • `TransactionPhase.BEFORE_COMMIT`
    • `TransactionPhase.AFTER_COMMIT` (기본값)
    • `TransactionPhase.AFTER_ROLLBACK`
    • `TransactionPhase.AFTER_COMPLETION`
    @Service
    public PushNotificationService {
        
        @TransactionalEventListener
        public void handleCreateMemberEvent(final CreateMemberEvent event) { ... }
        
        @TransactionalEventListener
        public void handleLoginMemberEvent(final LoginMemberEvent event) { ... }
    }

     

    비동기 이벤트

    스프링 프레임워크는 비동기 프로그래밍을 위해 `@Async`, `@EnableAsync`를 제공한다. 1) 어플리케이션에 비동기 실행 환경을 구성하고, 2) 비동기로 실행하고 싶은 메서드 앞에 `@Async` 어노테이션을 붙여주면 된다.

     

    먼저 구성 클래스 앞에 `@EnableAsync`를 통해 비동기 실행 환경을 활성화한다. 그리고 스레드 풀을 생성하여 멀티 스레드 환경을 만든다.

    @EnableAsync // 비동기 실행 활성화
    @Configuration
    public class AsyncConfig implements AsyncConfigurer {
    	
        @Override
    	public Executor getAsyncExecutor() {
        	return getThreadPoolTaskExecutor();
        }
        
        private TaskExecutor getThreadPoolTaskExecutor() {
        	ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
            threadPoolTaskExecutor.setCorePoolSize([CORE_POOL_SIZE]);
            threadPoolTaskExecutor.setMaxPoolSize([MAX_POOL_SIZE]);
            threadPoolTaskExecutor.setThreadNamePrefix("[THREAD_NAME_PREFIX]");
            return threadPoolTaskExecutor;
        }
    }

    다음으로 비동기로 실행할 메서드 앞에 `@Async`를 붙여준다. 이때, `@Async`는 AOP로 동작하므로 해당 메서드를 정의한 클래스는 반드시 빈이어야 하며, 메서드는 `public`이어야 한다.

    @Service
    public PushNotificationService {
        
        @Async // 해당 메서드를 비동기로 실행한다
        @TransactionalEventListener
        public void handleCreateMemberEvent(final CreateMemberEvent event) { ... }
        ...
    }

    참고자료

    댓글