[Spring] ApplicationEventPublisher를 이용한 이벤트 프로그래밍

2021. 4. 21. 02:13 Spring Framework/Spring Core

[Spring] ApplicationEventPublisher를 이용한 이벤트 프로그래밍

ApplicationEventPublisher는 Spring의 ApplicationContext가 상속하는 인터페이스 중 하나이다.

옵저버 패턴의 구현체로 이벤트 프로그래밍에 필요한 기능을 제공해준다.

 

1. ApplicationEvent를 상속하는 이벤트 클래스 만들기

import org.springframework.context.ApplicationEvent;
 
public class MyEvent extends ApplicationEvent {
 
    private int data;
 
    public MyEvent(Object source) {
        super(source);
    }
 
    public MyEvent(Object source, int data) {
        super(source);
        this.data = data;
    }
 
    public int getData() {
        return data;
    }
}

 

Spring 4.2 미만에서는 이벤트를 만들기 위해 ApplicationEvent를 상속받아야 한다.

위 예제 이벤트 MyEvent는 이벤트를 발생시킨 소스(source)와 정수 데이터를 실어서 전달할 수 있도록 하였다.

Spring에서 이벤트는 빈으로 등록하지 않는다.

 

2. ApplicationConext로 이벤트 발생시키기

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
 
@Component
public class AppRunner implements ApplicationRunner {
 
    @Autowired
    ApplicationEventPublisher eventPublisher;
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        eventPublisher.publishEvent(new MyEvent(this, 100));    // 이벤트 발생시키기
    }
}

 

ApplicationContext(ApplicationEventPublisher)의 publishEvent() 메소드를 호출해서 이벤트를 발생시킬 수 있다.

ApplicationContext 타입으로 주입받아도 되지만 이벤트 발생 기능을 사용할 것이므로 ApplicationEventPublisher 타입으로 선언하였다.

 

3. 이벤트 핸들링(처리) 하기

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
 
@Component
public class MyEventHandler implements ApplicationListener<MyEvent> {
    
    @Override
    public void onApplicationEvent(MyEvent event) {
        System.out.println("First event handling, data: " + event.getData());
    }
}

 

ApplicationEventPublisher가 발생시킨 이벤트를 처리할 핸들러 MyEventHandler 클래스를 생성하고 위와 같이 작성한다.

이벤트 핸들러는 spring이 발생한 이벤트를 누구에게 전달해야하는지 알아야 하기 때문에 빈으로 등록해야 한다.

 

Spring 4.2 미만에서는 이벤트 핸들러를 만들려면 ApplicationListener를 구현해주어야 한다.

 

onApplicationEvent() 안에 이벤트에 대해 필요한 작업을 처리하는 코드를 작성하면 된다.

 

실행 결과

 

어플리케이션을 구동하면 ApplicationRunner에서 호출한 ApplicationEventPublisher의 publishEvent()로 이벤트가 발생하고, Spring IoC에 등록돼있는 빈 중 이 이벤트를 처리할 handler 빈이 이벤트를 받아 처리한다.

 

4. Spring 4.2 이후의 이벤트 프로그래밍

1) 이벤트 클래스 변경

Spring 4.2부터는 이벤트가 ApplicationEvent를 상속받을 필요가 없다.

위에서 작성한 MyEvent 클래스를 다음과 같이 변경한다.

public class MyEvent {
 
    private Object source;
    private int data;
 
    public MyEvent(Object source) {
        this(source, 0);
    }
 
    public MyEvent(Object source, int data) {
        this.source = source;
        this.data = data;
    }
 
    public Object getSource() {
        return source;
    }
 
    public int getData() {
        return data;
    }
}

 

변경 후의 MyEvent는 spring 패키지를 전혀 사용하지 않는 POJO(Plain Old Java Object)가 된다.

 

2) 핸들러 클래스 변경 - @EventListener

이벤트와 마찬가지로 Spring 4.2부터는 이벤트가 ApplicationListener를 구현할 필요가 없다.

위에서 작성한 MyEventHandler 클래스를 다음과 같이 변경한다.

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
 
@Component
public class MyEventHandler{
 
    @EventListener
    public void onMyEvent(MyEvent event) {
        System.out.println("First event handling, data: " + event.getData());
    }
}

 

ApplicationListener를 구현하는 대신 @EventListener 애노테이션을 붙여준다.

더 이상 ApplicationListener의 메소드를 구현하는게 아니므로 이벤트를 처리하는 메소드 명은 자유롭게 명명할 수 있다.

 

이렇게 spring 4.2부터는 이벤트 클래스에 spring 소스 코드가 전혀 필요 없게 되고 이벤트 핸들러에는 @EventListener 애노테이션만 붙여주면 된다.

이러한 구현 방식은 소스 코드에 spring 코드가 들어가지 않도록, 노출되지 않도록 하는 spring의 '비침투성' 철학과 일치한다.

이런 것이 POJO 기반 프로그래밍이며 이는 개발자로 하여금 더 편하게 테스트할 수 있게 하고 코드를 유지보수하기 쉽게 한다.

 

코드 변경 후 실행 결과는 동일하다.

 

5. 여러 개의 이벤트 핸들러

MyEvent를 처리하는 다른 핸들러를 새로 만든다.

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
 
@Component
public class AnotherEventHandler {
 
    @EventListener
    public void onMyEvent(MyEvent event) {
        System.out.println("Another event handling, data: " + event.getData());
    }
}

 

실행 결과

이벤트 핸들러가 여러 개일 경우 기본적으로 하나의 thread에서 순차적(하나의 핸들러가 이벤트를 처리하고 나면 다른 핸들러가 처리)으로 실행된다.

 

이를 확인해보기 위해 이벤트 핸들러에 Thread 출력 코드를 추가한다.

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
 
@Component
public class MyEventHandler{
 
    @EventListener
    public void onMyEvent(MyEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("First event handling, data: " + event.getData());
    }
}
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
 
@Component
public class AnotherEventHandler {
 
    @EventListener
    public void onMyEvent(MyEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("Another event handling, data: " + event.getData());
    }
}

 

실행 결과

실행 결과로 둘 다 하나의 thread(main)에서 실행됨을 알 수 있다.

 

1) 이벤트 핸들러에 순서 부여하기 - @Order

이벤트 처리에 순서가 중요하다면 @Order 애노테이션을 사용해서 처리 순서를 부여할 수 있다.

import org.springframework.context.event.EventListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
 
@Component
public class MyEventHandler{
 
    @EventListener
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public void onMyEvent(MyEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("First event handling, data: " + event.getData());
    }
}

 

Ordered.HIGHEST_PRECEDENCE를 설정하면 가장 높은 우선순위를 갖게 된다.

import org.springframework.context.event.EventListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
 
@Component
public class AnotherEventHandler {
 
    @EventListener
    @Order(Ordered.HIGHEST_PRECEDENCE + 2)
    public void onMyEvent(MyEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("Another event handling, data: " + event.getData());
    }
}

 

@Order 애노테이션에 설정하는 값이 클수록 우선순위가 낮아진다.

따라서 @Order를 이렇게 설정하면 MyEventHandler가 먼저 이벤트를 처리하고 AnotherEventHandler가 나중에 처리하게 된다.

 

참고로 @Order() 애노테이션은 이벤트 핸들러 뿐만 아니라 다양한 곳에서 사용될 수 있는 애노테이션이다.

 

실행 결과

 

2) 비동기적 이벤트 핸들링

이벤트 핸들러는 기본적으로 동기적으로 실행된다.

비동기적으로 처리되게 하려면 우선 이벤트 처리 메소드에 @Async 애노테이션을 붙인다.

import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
 
@Component
public class MyEventHandler{
 
    @EventListener
    @Async
    public void onMyEvent(MyEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("First event handling, data: " + event.getData());
    }
}
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
 
@Component
public class AnotherEventHandler {
 
    @EventListener
    @Async
    public void onMyEvent(MyEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("Another event handling, data: " + event.getData());
    }
}

 

다음으로 @Configuration 클래스에 @EnableAsync 애노테이션을 붙인다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
 
@SpringBootApplication
@EnableAsync
public class Demospring52Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Demospring52Application.class, args);
    }
}

 

실행 결과

각 이벤트 핸들러가 비동기로 서로 다른 thread에서 실행되었다.

 

6. Spring 제공 이벤트

Spring이 기본으로 제공하는 이벤트들이 있다.

 

이벤트 설명
ContextRefresedEvent ApplicationContext를 초기화하거나 refresh할 때 발생
ContextStartedEvent ApplicationContext를 start()하여 라이프 사이클 빈들이 시작 신호를 받은 시점에 발생
ContextStoppedEvent ApplicationContext를 stop()하여 라이프 사이클 빈들이 정지 신호를 받은 시점에 발생
ContextClosedEvent ApplicationContext를 close()하여 싱글톤 빈이 소멸되는 시점에 발생
RequestHandledEvent HTTP 요청을 처리했을 때 발생

 

Spring이 제공해주는 이벤트들에 대한 핸들러를 구현해서 이벤트 발생 시 필요한 작업을 할 수 있다.

import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
 
@Component
public class MyEventHandler{
 
    @EventListener
    @Async
    public void onMyEvent(MyEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("First event handling, data: " + event.getData());
    }
 
    @EventListener
    @Async
    public void onContextRefreshedEvent(ContextRefreshedEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("ContextRefreshedEvent");
    }
 
    @EventListener
    @Async
    public void onContextClosedEvent(ContextClosedEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("ContextClosedEvent");
    }
}

 

MyEventHandler에 ContextRefresedEvent와 ContextClosedEvent를 처리하는 메소드를 추가하였다.

 

각 이벤트가 제대로 처리되는지 확인하기 위해 어플리케이션을 디버그 모드로 실행하고 종료해보자.

 

실행 결과

 

References

인프런 - 백기선님의 스프링 프레임워크 핵심 기술

 

출처 : atoz-develop.tistory.com/entry/Spring-ApplicationEventPublisher%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D?category=869243