1.15.2 표준과 커스텀 이벤트 by ks

ApplicationContext 에서 이벤트를 핸들링하는 것은 ApplicationEvent 클래스와 ApplicationListener 인터페이스를 통해 제공된다. 만약 ApplicationListener 인터페이스를 확장한 빈이 컨텍스트에 디플로이 된다면, ApplicationEvent가 갖는 매 시간 빈이 공지하는 ApplicationContext 에 퍼블리시 된다. 필수적으로, 이는 표준 옵저버 디자인 패턴이다.

참조 옵저버패턴 이미지. 한마디로 관찰자란 건데 옵저버가 무언가의 응답을 기다리고, 응답이 돌아오면 자신과 연결된 오브젝트들에게 응답을 전달한다.

스프링 4.2에서 이벤트 구조는 상당히 향상되었고, annotation-based model 을 어떠한 임의의 이벤트(즉, 필수적으로 ApplicationEvent를 확장하지 않는 오브젝트인) 를 게시할 수 있는 기능을 제공한다. 그런 오브젝트가 퍼블리시될 때, 우리는 이벤트를 감싼다.

아래 테이블은 스프링이 제공하는 표준을 설명한 것이다.

아래는 퍼블리시 시점에 따라 다른 이벤트 서술!

Table 7. Built-in Events

Event

Explanation

ContextRefreshedEvent

ApplicationContext가 초기화되고 리프레시(예를 들어서, refresh() 메소드를 ConfigurableApplicationContext 인터페이스에서 사용할 때)될 때 퍼블리시된다. 여기에, "초기화된" 이라는 것은 post-processor빈들이 탐지되고 활성화 되고, 싱글톤들이 pre-초기화되어 모든 빈들이 로드된 것을 의미하고, ApplicationContext 오브젝트가 사용할 준비가 된 것을 의미한다. 컨텍스트가 닫히지 않을 때까지 오래 리프레시는 여러번 트리거될 수 있고, ApplicationContext가 사실 "hot" 리프레시를 지원하여 제공될 수 있다. 예를 들어서 XmlWebApplicationContext가 핫 리프레시를 지원하지만 GenericApplicationContext는 아니다.

ContextStartedEvent

ApplicationContext가 ConfigurableApplicationContext 인터페이스의 start() 메소드를 사용해서 시작될 때 퍼블리시된다. 여기에 "started"란 의미는 모든 Lifecycle 빈이 명백한 시작 사인을 받은 것을 의미한다. 전형적으로 이 신호는 명백한 멈춤 후에 빈을 재시작하는데 사용되지만, 자동시작(예를 들어서, 초기화하면서 미리 시작하지 않은 컴포넌트들)을 설정하지 않았던 컴포넌트를 시작하는데 사용되기도 한다.

ContextStoppedEvent

ApplicationContext가 ConfigurableApplicationContext 인터페이스에서 stop() 메소드를 사용하여 멈출 때 퍼블리시된다. 여기에, "stopped"란 것은 모든 Lifecycle 빈들이 명백한 멈춤 신호를 받는 다는 것을 의미한다. 멈춘 컨텍스트는 start() 콜로 재시작할지도 모른다.

ContextClosedEvent

ApplicationContext가 ConfigurableApplicationContext 인터페이스에서 close() 메소드를 사용하여 닫힐 때 퍼블리시된다. 여기에 "닫히다"라는 것은 모든 싱글톤 빈이 파괴된다는 것을 의미한다. 닫힌 컨텍스트는 생명주기의 끝에 도달한다. 리프레시 되거나 재시작될 수 없다.

RequestHandledEvent

웹-구체화한 이벤트는 HTTP 요청을 서비스하는 모든 빈들을 이야기한다. 이 이벤트는 리퀘스트가 완료된 후에 퍼블리시된다. 이 이벤트는 오직 DispatcherServlet 을 사용한 웹 어플리케이션에 적용된다.

커스텀 이벤트를 생성하고 퍼블리시할 수 있다. 스프링의 ApplicationEvent 기반 클래스를 확장하는 간단한 클래스를 보여준다.

public class BlackListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlackListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}

커스텀 ApplicationEvent 을 퍼블리시하기 위해 ApplicationEventPublisher 에서 publishEvent()를 부른다. 전형적으로 이는 ApplicationEventPublisherAware를 확장하는 클래스를 생성하고 스프링 빈에 등록한다.

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blackList;
    private ApplicationEventPublisher publisher;

    public void setBlackList(List<String> blackList) {
        this.blackList = blackList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blackList.contains(address)) {
            publisher.publishEvent(new BlackListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

configuration에서 스프링 컨테이너는 ApplicationEventPublisherAware를 확장한 EmailService를 탐지하고 자동적으로 setApplicationEventPublisher() 를 부른다. 실제로 전달된 파라미터는 스프링 컨테이너 그 자체이다. ApplicationEventPublisher 인터페이스를 통해서 어플리케이션 컨텍스와 상호작용한다.

커스텀 ApplicationEvent 를 받기 위해, ApplicationListener 를 확장하는 클래스를 생성할 수 있고, 스프링빈에 등록할 수 있다.

public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

ApplicationListener 가 일반적으로 (예제에서 BlackListEvent) 커스텀 이벤트의 타입으로 파라미터된다. 이는 onApplicationEvent() 메소드가 다운캐스팅의 필요성을 피하는 type-safe를 유지할 수 있는 것을 의미한다. (확장하는 애들을 전부 반환할 수 있어서 원하는 만큼 등록할 수 있다는 의미!)많은 이벤트 리스너를 원하는 만큼 등록할 수 있지만 기본적으로 이벤트 리스너는 이벤트를 동기적으로 받는다. 이는 publishEvent() 메소드가 모든 리스너가 이벤트 처리를 끝내는 동안 막는 다는 것을 의미한다. 이 동기화와 싱글쓰레드의 한가지 장점은, 리스너가 한 이벤트를 받을 때, 만약 트랜잭션 컨텍스트가 사용가능하다면 퍼블리셔의 트랜젝션 컨텍스트 안에서 동작한다는 것을 의미한다. 만약 이벤트 퍼블리시를 위해 또다른 전략이 필요해진다면 ApplicationEventMulticaster 를 참조해라.

아래 예제는 각 클래스를 configure하고 등록하는 정의를 보여준다.

<bean id="emailService" class="example.EmailService">
    <property name="blackList">
        <list>
            <value>known.spammer@example.org</value>
            <value>known.hacker@example.org</value>
            <value>john.doe@example.org</value>
        </list>
    </property>
</bean>

<bean id="blackListNotifier" class="example.BlackListNotifier">
    <property name="notificationAddress" value="blacklist@example.org"/>
</bean>

emailService 빈의 sendEmail() 메소드가 불러질 때 블랙리스트에 포함되어야 할 이메일 메시지가 있는 경우 모두 함께 넣어서 커스텀 이벤트 BlackListEvent가 퍼블리시된다. blackListNotifier 빈은 ApplicationListener 로서 등록되고 적절한 파티에 공지할 수 있는 BlackListEvent 를 받는다.

스프링의 이벤트 메커니즘은 같은 어플리케이션 컨텍스트안에 스프링 빈들 사이에 간단한 커뮤니캐이션으로 디자인된다. 하지만, 더 복잡한 엔터프라이즈 통합 요구를 위해 별도로 유지 관리 되는 Spring Integration 프로젝트는 잘 알려진 스프링 프로그래밍 모델을 기반으로 하는 가벼운 패턴 기반의 이벤트 중심 아키텍쳐를 완벽하게 지원한다.

어노테이션 기반의 이벤트 리스너

스프링 4.2에서 EventListener 어노테이션을 사용해서 관리하는 빈의 어떠한 public 메소드에 이벤트 리스너를 등록할 수 있다.BlackListNotifier 는 아래와 같 쓰여질수 있다.

public class BlackListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlackListEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

메소드 서명은 다시 리슨하는 이벤트 유형을 선언하지만, 이번에는 유연한 이름으로 특정 리스너 인터페이스를 구현하지 않고 선언한다. 실제 이벤트 유형이 구현 계층에서 일반 매개 변수를 해결하는 한 이벤트 유형은 제네릭을 통해 축소 될 수도 있다. 메소드가 여러 이벤트를 청취해야하거나 매개 변수없이 이벤트를 정의하려는 경우 이벤트 유형을 주석 자체에 지정할 수도 있습니다. 다음 예제에서는이를 수행하는 방법을 보여다.

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    ...
}

또한 특정 이벤트에 대한 메소드를 실제로 호출하기 위해 일치해야하는 SpEL 표현식을 정의하는 어노테이션의 condition 속성을 이용해서 런타임 필터링을 추가할 수 있다.

SpEL 표현식 : Spring Expression Language. 런타임시에 객체 그래프를 조회하고 조작하는 표현 언어.

참조 : http://wonwoo.ml/index.php/post/1940

다음 예제는 content 이벤트 속성이 다음 (my-event) 과 같은 경우에 호출자를 다시 작성하는 방법을 보여준다.

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlackListEvent(BlackListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

각 SpEL 표현은 가리키는 컨텍스트에 대응하여 평가한다. 아래 테이블은 컨텍스트에 사용가능한 아이템들 리스트이고, 조건부 이벤트에 사용할 수 있다.

Table 8. Event SpEL available metadata

Name

Location

Description

Example

Event

root object

ApplicationEvent.

#root.event

Arguments array

root object

타겟을 부르는데 사용되는 인자(배열)

#root.args[0]

Argument name

evaluation context

메소드 인자 이름. 만약 몇몇 이유로 이름들이 사용불가능해지면(예를 들어서 디버그 정도가 없을때), 인자 이름은 #arg가 인자 인텍스로서 (0부터 시작)#a<#arg>아래에서 사용할 수 있다.

#blEvent or #a0 (you can also use #p0 or #p<#arg>notation as an alias)

비록 메소드 서명이 실제로 퍼블리시된 임의의 오브젝트를 참조할수 있더라도 #root.event는 밑에있는 이벤트로 접근하도록 한다.

만약 또다른 이벤트를 처리하는 결과로서 이벤트를 퍼블리시할 필요성이 있다면 메소드 서명을 퍼블리시 되어야 하는 이벤트를 반환하도록 변경할 수 있다.

@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

이 기능은 asynchronous listeners.를 지원하지 않는다.

이 새로운 매소드는 메소드에 의해 핸들되는 모든 BlackListEvent를 위해 새로운 ListUpdateEvent를 퍼블리시하지 않는다. 만약 몇 이벤트를 퍼블리시할 필요성이 있다면 이벤트 대신에 Collection를 반환할 수 있다.

비동기 리스너

만약 부분적인 리스너가 비동기적으로 이벤트를 처리한다면 regular @Async support.를 재사용할 수 있다.

@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
    // BlackListEvent is processed in a separate thread
}

비동기 이벤트를 사용할 때 한계가 있다.

  • 만약 이벤트 리스너가 Exception을 던지면 호출자에게 전달되지 않는다. 더 상세한 사항을 위해 AsyncUncaughtExceptionHandler를 보자.

  • 그런 이벤트 리스너는 응답을 보낼 수 없다. 만약 처리 결과로서 또다른 이벤트를 보낼 필요가 있다면 수동으로 이벤트를 보내기 위해 ApplicationEventPublisher 를 주입하자.

리스너 순서

만약 한 리스너가 또다른 하나 전에 불러질 필요가 있다면, @Order 어노테이션을 메소드 선언에 추가할 수 있다.

@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress...
}

제네릭 이벤트

이벤트의 구조를 심층적으로 정의하기 위해 제네릭을 사용할 수 있다. EntityCreatedEvent<T> 에서 T는 생성된 실제 엔티티의 타입이고 이걸 사용하는 것을 고려해보자. 예를 들어서 아래 리스너 정의는 Person에서 EntityCreateEvent를 받기위한 정의이다.

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    ...
}

유형 삭제로 인해 발생하는 이벤트가 이벤트 리스너가 필터링하는 일반 매개 변수 (즉, PersonCreatedEvent 클래스가 EntityCreatedEvent {...}을 확장하는 것과 같은 것)를 해결하는 경우에만 작동한다.

특정 상황에서는 모든 이벤트가 동일한 구조를 따르는 경우 (앞의 예에서 이벤트의 경우처럼) 매우 지루할 수 있다. 이 경우 ResolvableTypeProvider를 구현하여 런타임 환경에서 제공하는 것 이상으로 프레임 워크를 안내 할 수 있다. 다음 이벤트는 그렇게하는 방법을 보여준다

이러한 정보는 Java가 런타임에 서명을 전달하지 않으므로 인스턴스가 일반 서명과 일치하는지 파악할 때 매우 유용합니다.

이 인터페이스 사용자는 특히 클래스의 제네릭 형식 서명이 하위 클래스에서 변경되는 경우 복잡한 계층 시나리오에서주의해야합니다.

참조 : https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/ResolvableTypeProvider.html

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}

이는 ApplicationEvent 뿐 아니라 이벤트로서 보낸 임의의 오브젝트에도 동작한다.

Last updated