[스프링 IoC 컨테이너와 빈]_09 ApplicationEventPublisher

4 minute read

ApplicationEventPublisher

이벤트 프로그래밍에 필요한 인터페이스 제공. 옵저버 패턴 구현체

ApplicationContext extends ApplicationEventPublisher

● publishEvent(ApplicationEvent event)  

이벤트 만들기

● ApplicationEvent 상속  
● 스프링 4.2 부터는 이 클래스를 상속받지 않아도 이벤트로 사용할 수 있다.  

이벤트 발생 시키는 방법

● ApplicationEventPublisher.publishEvent();  

이벤트 처리하는 방법

● ApplicationListener<이벤트> 구현한 클래스 만들어서 빈으로 등록하기.  
● 스프링 4.2 부터는 [@EventListener](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/context/event/EventListener.html)를 사용해서 빈의 메소드에 사용할 수 있다.  
● 기본적으로는 synchronized.  
● 순서를 정하고 싶다면 @Order와 함께 사용.  
● 비동기적으로 실행하고 싶다면 @Async와 함께 사용.  

스프링이 제공하는 기본 이벤트

● ContextRefreshedEvent: ApplicationContext를 초기화 했더나 리프래시 했을 때 발생.  
● ContextStartedEvent: ApplicationContext를 start()하여 라이프사이클 빈들이 시작  

신호를 받은 시점에 발생.

● ContextStoppedEvent: ApplicationContext를 stop()하여 라이프사이클 빈들이 정지  

신호를 받은 시점에 발생.

● ContextClosedEvent: ApplicationContext를 close()하여 싱글톤 빈 소멸되는 시점에 발생.  
● RequestHandledEvent: HTTP 요청을 처리했을 때 발생.  

스프링부트 4.2 이전의 이벤트 처리

1. 이벤트 생성

이 이벤트는 빈으로 등록되는 것이 아니다.

스프링부트 4.2 이전 버전에서는 항상 ApplicationEvent를 상속받았어야 했다.

다음 코드는 이벤트를 발생시킨 source만 전달하고 있다.

public class MyEvent extends ApplicationEvent {
    /**
     * Create a new {@code ApplicationEvent}.
     *
     * @param source the object on which the event initially occurred or with
     *               which the event is associated (never {@code null})
     */
    public MyEvent(Object source) {
        super(source);
    }
}

원하는 데이터를 담아서 전송하는 기능의 이벤트를 생성

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;
    }
}

2. 이벤트 발생

  • ApplicationEventPublisher.publishEvent();

이벤트를 Publish하는 기능을 ApplicationEventPublisher가 가지고 있다.

ApplicationContext가 ApplicationEventPublisher를 상속받았기 때문에 ApplicationContext로 이벤트를 발생시킬 수 있다.

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    ApplicationEventPublisher publisherEvent;
//    ApplicationContext applicationContext;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        publisherEvent.publishEvent(new MyEvent(this, 100)); //이벤트 발생
//        applicationContext.publishEvent(new MyEvent(this, 100));
    }
}

3. 이벤트 핸들러

발생한 이벤트를 받아서 처리

이벤트 핸들러는 빈으로 등록이 되어야한다.

스프링부트 4.2 이전에는 ApplicationListener<>라는 인터페이스를 구현했어야 했다.

( 4.2 이후 부터는 ApplicationListener를 구현해야하는 제약사항이 사라졌다. )

이벤트가 발생하면 등록되어있는 빈들 중 MyEventHandler가 받아서 처리한다.

@Component
public class MyEventHandler implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        //전달받은 event로 원하는 작업을 하면 된다.
        System.out.println("이벤트 전달 받음!!!! 데이터는 : " + event.getData());
    }
}

이벤트핸들러

스프링부트 4.2 이후의 이벤트 처리 (추천)

  • ApplicationEvent을 구현해야하는 제약사항이 사라짐
  • "비침투성"을 지키며 코드를 작성할 수 있게 됨

1. 이벤트 생성

ApplicationEvent를 구현하지 않고 이벤트를 생성하도록 코드를 수정해보자

(※ 이벤트는 빈이 아님!!)

public class MyEvent {

    private int data;

    private Object source;

    public MyEvent(Object source, int data) {
        this.source = source;
        this.data = data;
    }

    public Object getSource() {
        return source;
    }

    public int getData() {
        return data;
    }
}

2. 이벤트 핸들러

  • @EventListener 어노테이션

ApplicationListener<>라는 인터페이스를 구현하지 않아도 되는 대신, 스프링이 핸들러를 알아야하므로 빈으로 등록해줘야 한다.

@Component
public class MyEventHandler {

    @EventListener
    public void handle(MyEvent event) {
        System.out.println("이벤트 전달 받음!!!! 데이터는 : " + event.getData());
    }
}

⇒ 실행하면 정상적으로 동작한다.

⇒ 이게 바로 스프링이 추구하는 철학이다. “비침투성”

내 코드에 스프링 프레임워크 코드가 들어가 있지 않다. 이게 가장 깔끔한 포조이다. ⇒ “포조기반 프로그래밍”

테스트할 때 더 편하고, 유지보수하기 좋아진다.


다수개 핸들러가 존재할 때 이벤트 처리

이벤트 핸들러가 여러가지 있을 때, 두 핸들러 모두 실행된다. 기본적으로 순차적으로 실행된다. 뭐가 먼저 실행될지는 모르지만, 한 이벤트가 실행된 후 다음 이벤트가 실행된다. 즉, 두 핸들러를 다른 스레드로 동시에 실행시켜주지 않는다는 의미

※ [실습]

또 다른 핸들러를 생성

@Component
public class AnotherHandler {

    @EventListener
    public void handle(MyEvent myEvent) {
        System.out.println(Thread.currentThread().toString()); //현재 스레드정보 출력
        System.out.println("Another Handler !!! 데이터는 : " + myEvent.getData() );
    }
}

두개의 핸들러

⇒ main 스레드로 순차적으로 두 핸들러가 모두 실행된다.

순서를 정해서 이벤트 처리

  • EventHandler에 @Order어노테이션으로 우선순위를 부여하면 된다.

  • @Order(Ordered.HIGHEST_PRECEDENCE)
  • @Order(Ordered.HIGHEST_PRECEDENCE + n) : 우선순위에 값을 더해주면 순위가 밀린다.

MyEventHandler

@Component
public class MyEventHandler {
    @EventListener
    @Order(Ordered.HIGHEST_PRECEDENCE) //<- 더 높은 우선순위
    public void handle(MyEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("이벤트 전달 받음!!!! 데이터는 : " + event.getData());
    }
}

AnotherHandler

@Component
public class AnotherHandler {

     @EventListener
     @Order(Ordered.HIGHEST_PRECEDENCE + 2) //<- 더 낮은 우선순위
    public void handle(MyEvent myEvent) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("Another Handler !!! 데이터는 : " + myEvent.getData() );
    }
}

⇒ 실행결과

순서대로실행

비동기적으로 이벤트 실행

각각의 스레드 풀에서 돌고, 어떤 스레드가 먼저 실행될지는 스레드 스케쥴링에 따라서 결정되기 때문에 @Order로 순서를 지정하는 것은 의미가 없다.

  • 핸들러에 @Async 어노테이션을 붙인다.

    @Async 어노테이션만 붙인다고 비동기적으로 실행되진 않는다.

  • main 클래스에 @EnableAsync를 붙인다.

    (스레드 관련 설정은 더 있지만 현재는 간략하게 디폴트 스레드 풀에서 동작하도록 한다.)

@Component
public class AnotherHandler {

    @EventListener
    @Async //<-- *
    public void handle(MyEvent myEvent) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("Another Handler !!! 데이터는 : " + myEvent.getData());
    }
}
@SpringBootApplication
@EnableAsync //<-- *
public class Demospring51Application {

    public static void main(String[] args) {
        SpringApplication.run(Demospring51Application.class, args);
    }

}

비동기실행

⇒ 각각의 스레드에서 동작한다.


스프링이 제공하는 기본 이벤트

스프링이 기본으로 제공해주는 ApplicationContext관련 이벤트가 있다.

  • ContextRefreshedEvent: ApplicationContext를 초기화 했더나 리프래시 했을 때 발생.
  • ContextStartedEvent: ApplicationContext를 start()하여 라이프사이클 빈들이 시작

스프링이 기본으로 제공해주는 ApplicationContext관련 이벤트를 처리해주는 핸들러 추가

@Component
public class MyEventHandler {
    @EventListener
    @Async
    public void handle(MyEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("이벤트 전달 받음!!!! 데이터는 : " + event.getData());
    }

    @EventListener
    @Async
    public void handle(ContextRefreshedEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("ContextRefreshedEvent");
        ApplicationContext applicationContext = event.getApplicationContext(); //직접 ApplicationContext를 꺼내서 사용할 수도 있다.
        System.out.println("==>>> " + applicationContext.getClass());
    }

    @EventListener
    @Async
    public void handle(ContextClosedEvent event) {
        System.out.println(Thread.currentThread().toString());
        System.out.println("ContextClosedEvent");
    }
}

스프링이 제공하는 기본 이벤트

⇒ 스프링부트가 이러한 여러 이벤트들을 확장해서 더 많이 제공해준다. (→ 자세한 내용은 스프링부트 강좌에서)