스프링 이벤트는 어떻게 동작할까?

심규민·2024년 7월 9일

스프링 이벤트란?

스프링 이벤트는 스프링에서 제공하는 메커니즘으로 컴포넌트간 느슨한 결합을 통해 상호작용할 수 있는 방법이다.
스프링 이벤트는 옵저버 패턴을 기반을 두고 있다. 이는 publisher와 subscriber가 있으며, publisher의 상태가 변경되면, 해당 변경에 대해 subscriber에게 subject 객체를 전달하는 디자인 패턴이다.
스프링에서는 ApplicationEvent 클래스의 인스턴스를 통해 이벤트를 발생한다.

스프링 이벤트 구성요소

스프링 이벤트는 다음과 같은 core 클래스들을 제공한다.

  • ApplicationListener: jdk에 기본으로 포함된 EventListener을 확장한 인터페이스로, 이벤트 수신시 onApplicationEvent 호출을 통해 실행된다.
  • ApplicationEvent: jdk에 기본으로 포함된 EventObject의 구현 클래스이며, 스프링 이벤트에서의 모든 이벤트 객체는 ApplicationEvent의 서브 클래스이다.
  • ApplicationEventMulticaster: ApplicationListener를 등록하거나 이벤트를 배포하는 역할을 가짐
  • ApplicationContext: 스프링에서의 IOC 컨테이너이며, publisher 역할도 한다. 이는 ApplicationContext가 ApplicationEventPublisher를 구현하기 때문임

위 클래스들을 통해 스프링 내부에서 이벤트 기반 프로그래밍을 가능하게 만든다.
스프링 이벤트 메커니즘을 통해 스프링 내부의 컴포넌트들은 서로의 존재를 모르더라도 상호작용할 수 있다.

위 클래스들을 기반으로 이벤트의 흐름을 나타내면 다음과 같다.

그러면 core 클래스들의 코드를 살펴봐보자

ApplicationEvent

public abstract class ApplicationEvent extends EventObject {
    private static final long serialVersionUID = 7099057708183571937L;
    private final long timestamp;

    public ApplicationEvent(Object source) {
        super(source);
        this.timestamp = System.currentTimeMillis();
    }

    public ApplicationEvent(Object source, Clock clock) {
        super(source);
        this.timestamp = clock.millis();
    }

    public final long getTimestamp() {
        return this.timestamp;
    }
}

ApplicationEvent는 스프링 이벤트에서 publish, consume에서 사용되는 기반 클래스이다.
또한 ApplicationEvent의 서브 클래스가 필요한 정보들을 초기화할 수 있는 생성자를 제공한다.
위 코드에서는 인스턴스 생성 시간과 함께, 이벤트 객체를 담을 수 있다.
또한 ApplicationEvent의 확장을 통해 특정 이벤트에서 사용하는 이벤트 객체를 만들 수 있다.

위 이미지에서는 ApplicationEvent의 서브 클래스들을 확인할 수 있다.

  • ServeltRequestHandleEvent: 이는 Spring MVC에서 @RequestMapping에 의해 요청이 처리된 후 발행되는 이벤트이다. 이는 ReuqestHandleEvent에 비해 더 다양한 정보를 제공해준다.
  • PayloadApplicationEvent: 이는 Spring 4.2 이후 ApplicationEvent의 하위 타입이 아닌 이벤트 객체에 대해 spring event를 지원하도록하기 위해 추가된 클래스다. 즉, ApplicationEvent의 하위 타입이 아닌 이벤트 객체가 발해오디면 PayloadApplicationEvent에 감싸져(wrapped)되며 발행된다.
  • ApplicationContextEvent: 이는 application context의 상태가 변경될 경우 발행되는 이벤트이다. 이는 application context의 lifecycle을 추적하고 context를 initialized, refreshed, 또는 close할 때 사용한다.

스프링 부트에서의 이벤트

스프링 부트는 스프링을 간단하게 설정하며 실행할 수 있도록 도와주는 기능을 제공한다. 이때 스프링 이벤트를 기본적으로 설정되어 있으며, 여러 이벤트를 제공한다.

  • ApplicationStartingEvent: 애플리케이션이 실행되며 application context가 생성되기 전에 발행되는 이벤트이다.
  • ApplicationEnvironmentPreparedEvent: application context가 생성된 후, 환경설정을 설정하기 전에 발행되는 이벤트이다.
  • ApplicationPreparedEvent: 환경 설정이 마무리된 후, application context가 refresh(이때 빈이 등록됨)되기 전에 발행되는 이벤트다.
  • ApplicationStartedEvent: 애플리케이션이 실행 설정이 모두 마무리되면 발행되는 이벤트다.
  • ApplicationReadyEvent: 요청을 받을 준비가 완료되면 발행되는 이벤트다.
  • ApplicationFailedEvent: 시작에 실패하면 발행되는 이벤트다.

스프링 애플리케이션이 실행되면 EventPublishingRunListener 클래스를 통해 이벤트를 발행한다.
EventPublishingRunListener 애플리케이션으로부터 현재 상태가 담긴 이벤트를 수신하면, SimpleApplicationEventMulticaster를 통해 이벤트를 전파한다.

class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    private final SpringApplication application;
    private final String[] args;
    private final SimpleApplicationEventMulticaster initialMulticaster;

    EventPublishingRunListener(SpringApplication application, String[] args) {
        this.application = application;
        this.args = args;
        this.initialMulticaster = new SimpleApplicationEventMulticaster();
    }


    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        this.multicastInitialEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args));
    }

    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        this.multicastInitialEvent(new ApplicationEnvironmentPreparedEvent(bootstrapContext, this.application, this.args, environment));
    }

    public void contextPrepared(ConfigurableApplicationContext context) {
        this.multicastInitialEvent(new ApplicationContextInitializedEvent(this.application, this.args, context));
    }

    public void contextLoaded(ConfigurableApplicationContext context) {
        ApplicationListener listener;
        for(Iterator var2 = this.application.getListeners().iterator(); var2.hasNext(); context.addApplicationListener(listener)) {
            listener = (ApplicationListener)var2.next();
            if (listener instanceof ApplicationContextAware contextAware) {
                contextAware.setApplicationContext(context);
            }
        }

        this.multicastInitialEvent(new ApplicationPreparedEvent(this.application, this.args, context));
    }

    public void started(ConfigurableApplicationContext context, Duration timeTaken) {
        context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context, timeTaken));
        AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
    }

    public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
        context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context, timeTaken));
        AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
    }


	public void failed(ConfigurableApplicationContext context, Throwable exception) {
		ApplicationFailedEvent event = new ApplicationFailedEvent(this.application, this.args, context, exception);
		if (context != null && context.isActive()) {
			// Listeners have been registered to the application context so we should
			// use it at this point if we can
			context.publishEvent(event);
		}
		else {
			// An inactive context may not have a multicaster so we use our multicaster to
			// call all the context's listeners instead
			if (context instanceof AbstractApplicationContext abstractApplicationContext) {
				for (ApplicationListener<?> listener : abstractApplicationContext.getApplicationListeners()) {
					this.initialMulticaster.addApplicationListener(listener);
				}
			}
			this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());
			this.initialMulticaster.multicastEvent(event);
		}
	}


    private void multicastInitialEvent(ApplicationEvent event) {
        this.refreshApplicationListeners();
        this.initialMulticaster.multicastEvent(event);
    }

    private void refreshApplicationListeners() {
		this.application.getListeners().forEach(this.initialMulticaster::addApplicationListener);
	}

}

EventPublishingRunListener는 SpringApplicationRunListener 인터페이스를 구현한다. 이 인터페이스는 스프링 애플리케이션 실행 과정에서의 호출할 수 있는 메소드를 제공한다.

EventPublishingRunListener의 생성자에서 볼 수 있듯, SimpleApplicationEventMulticaster 인스턴스를 생성한다.

스프링 이벤트를 어떻게 사용할까?

스프링 이벤트를 사용할 때 다음과 같은 용도로 사용할 수 있다.

  1. 특정 이벤트에 대한 특정 동작 실행 가능: 예를 들면 사용자가 로그인할 때 UserLoggedInEvent를 배포하여 로그를 남기거나, 최근 접속일 갱신등을 처리할 수 있다.
  2. 컴포넌트간 decoupling: 이벤트 발행 주체와 수신 주체는 서로 알지 않고도 특정 행위를 처리할 수 있음. 예를 들면 특정 조건에 대한 알림 전송과 같은 상황에서 사용할 수 있음
  3. auditing and logging: 데이터베이스의 특정 레코드 값이 변경될 떄 변경의 주체를 로그로 남길 때 사용할 수 있음.
  4. 캐시 무효화: 데이터 변경에 따른 캐시 값 무효화할 때 사용할 수 있음
  5. 외부 시스템과의 통합: 메시지 큐와 같은 외부 시스템간의 통합을 할 수 있음
    • Spring Modulith Externalized 에노테이션을 통해 더 쉽게 할 수 있음
  6. 비즈니스 규칙 검사: 예를 들면 주문이 들어올 때 주문 내용을 이벤트로 배포하면, 주문 검사 클래스가 해당 이벤트를 통해 주문을 검사할 수 있음
  7. 업무 자동화: 특정 주문이 들어오면 해당 주문을 이벤트로 배포하면, 해당 주문을 처리할 팀에 자동으로 배정할 수 있음

출처
Spring Application Event Mechanism

0개의 댓글