본문 바로가기
공부 기록

[SPRING] 스프링이 Event를 다루는 방법 - ApplicationEventPublisher

by 타태 2023. 5. 7.

2023.03.08 - [생각 정리] - [GOORM x COMMIT] 기술 부채를 바라보는 다른 시각 - 양수열

 

[GOORM x COMMIT] 기술 부채를 바라보는 다른 시각 - 양수열

https://blog.goorm.io/javaoracle/ 양수열 소장, “개발자 마음속의 짐 ‘기술 부채’를 덜어내려면” ‘기술 부채’는 개발자로 있는 한 마음속에 항상 품고 고민하는 주제입니다. 기술 부채가 없는 회

ktae23.tistory.com

 

** spring 5.3 버전을 참조했습니다.

 

스프링은 사용자가 직접 정의한 이벤트를 생성하거나 발행하는 것을 허용한다.

이를 이용하면 핵심 로직과 부가 로직을 분리하여 좀 더 유기적인 설계를 구현 하는데 도움을 받을 수 있다.

 

이를 위해 검색 해보면 보통은 Application Event Publisher에 대한 사용법, Application Evenet Listener 사용법에 대해서만 나오기 때문에 스프링에서 이벤트를 다루는 방식에 대해 정리해보고자 한다.

 

 

스프링에서 이벤트를 다루는 방법을 다루는 Baeldung 튜토리얼부터 시작해보자.

https://www.baeldung.com/spring-events

 

스프링은 커스텀한 이벤트의 생성과 발행을 제공하는데 이는 기본적으로 동기 방식으로 작동한다.

동기식으로 동작함으로써 이벤트를 발행 한 트랜잭션 컨텍스트에 참여할 수 있는 등의 몇 가지 이점이 있다.

 

스프링 4.2 버전 이전에는 ApplicationEvent를 확장하는 이벤트 객체를 정의하고, 이를 발행하면 구독하는 Listener interface 를 구현해서 해결하는 방식을 제공했다.

 

Baeldung의 튜토리얼도 이렇게 설명한다.

public class CustomSpringEvent extends ApplicationEvent {
    private String message;

    public CustomSpringEvent(Object source, String message) {
        super(source);
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}

// ====

@Component
public class CustomSpringEventPublisher {
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void publishCustomEvent(final String message) {
        System.out.println("Publishing custom event. ");
        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
        applicationEventPublisher.publishEvent(customSpringEvent);
    }
}

// ===
@Component
public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
    @Override
    public void onApplicationEvent(CustomSpringEvent event) {
        System.out.println("Received spring custom event - " + event.getMessage());
    }
}

 

하지만 스프링 4.2 버전 이후로는 어노테이션 주도 방식을 제공하기 때문에 이후 버전에서는 직접 확장 및 구현을 할 필요가 없다.

또한 기본적으로 동기 방식인 이벤트 리스너를 @Async 어노테이션을 추가하는 것 만으로 비동기 방식으로 변경 할 수 있는데 단지 EnableAsyncSupport 설정을 어플리케이션에 해두기만 하면 된다.

이 외에도 이벤트에 제네릭을 지원하는 점과 SPEL을 지원하여 아래와 같은 조건부 호출이 가능하다.

@Component
public class AnnotationDrivenEventListener {
    @EventListener(condition = "#event.success")
    public void handleSuccessful(GenericSpringEvent<String> event) {
        System.out.println("Handling generic event (conditional).");
    }
}

 

또한 @EventListener 어노테이션이 달린 메서드에서 null이 아닌 값을 반환하면 해당 결과를 새 이벤트로 발행하며 컬렉션으로 반환할 경우 여러 개의 새 이벤트를 게시할 수도 있다.

 

마지막으로 설명할 리스너의 기능은 @TransactionalEventListener이다.

이 어노테이션 역시 스프링 4.2부터 제공을 하는데 이는 @EventListener을 확장하여 리스너에 이벤트의 트랜잭션 수준을 결합하는 것을 허용한다.

 

자세한 설명은 본 글의 취지와 맞지 않으니 참고할만한 블로그를 적어두는 정도로 넘어가겠다.

모두 좋은 글이니 참고해보면 좋겠다.

https://findstar.pe.kr/2022/09/17/points-to-consider-when-using-the-Spring-Events-feature/

https://wildeveloperetrain.tistory.com/246

https://blog.codeleak.pl/2017/10/asynchrouns-and-transactional-event.html

 


 

이처럼 이벤트를 사용하는 방식은 빈과 빈 사이의 의존성 주입을 통한 메서드 호출이 아닌 이벤트를 발행하여 Application Context로 이를 넘기고 Listener가 이를 구독하는 방식이다.

때문에 패키지 구조는 아래와 같다.

 

대부분 우리는 커스텀하여 이벤트를 사용하느라 편리한 기능 부수 기능 정도로 생각할 수 있지만 무려 context 패키지에 있는 기능이다.

때문에 실제로 spring에는 개발자가 애플리케이션 및 컨텍스트의 수명 주기에 연결되는 사용자 지정 작업을 수행 할 수 있도록 ApplicationContextEvnet를 확장한 다양한 기본 제공 이벤트가 있다.

 

하지만 이러한 이벤트를 수동으로 사용하는 경우는 거의 없으므로 참고만 하자

https://www.baeldung.com/spring-context-events

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#context-functionality-events

 


이러한 이벤트를 발행시키는 녀석은 ApplicationEventPublisher인데 사용하는 인터페이스는 함수형으로 매우 단순하다.

package org.springframework.context;

@FunctionalInterface
public interface ApplicationEventPublisher {
    default void publishEvent(ApplicationEvent event) {
        this.publishEvent((Object)event);
    }

    void publishEvent(Object event);
}

 

이 메서드를 사용하는 대상 중 

EventPublicationInterceptor, PayloadApplicationEvent, AbstractApplicationContext 세 클래스가 주목할 만하다.

 

이중 AbstractApplicationContext를 먼저 살펴보겠다.

이 추상 클래스는 DefaultResourceLoader 를 확장하고 ConfigurableApplicationContext를 구현한다.


이 중 ConfigurableApplicationContextApplicationContext, Lifecycle, Closeable를 확장하는 인터페이스인데 ApplicationContext가 EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver를 확장하는 인터페이스이기 때문에 publishEvent를 오버라이드 하는 것이다.

 

ApplicationEventPublisher ApplicationEventPublisherAware를 통해 컨텍스트에 세팅되는데 ApplicationEventPublisher를 설정하는 setApplicationEventPublisher()ApplicationContextAwareProcessor에서 호출된다.

private void invokeAwareInterfaces(Object bean) {
   // 생략
    if (bean instanceof ApplicationEventPublisherAware) {
        ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
    }
   // 생략
}

 

또한 AplicationContext에는 이런 주석이 달려 있다.

>In addition to standard {@link org.springframework.beans.factory.BeanFactory}
 * lifecycle capabilities, ApplicationContext implementations detect and invoke
 * {@link ApplicationContextAware} beans as well as {@link ResourceLoaderAware},
 * {@link ApplicationEventPublisherAware} and {@link MessageSourceAware} beans.

 AplicationContext 생명주기에 따라 ApplicationEventPublisherAware가 실행되며 ApplicationEventPublisher가 설정된다는 의미이다.

 

때문에 오버라이드 한 메서드들은 아래와 같이 컨텍스트 생명주기에 따른 이벤트를 발행시키거나 이벤트 타입에 따라 PayloadAppliactionEvent로 변환하여 처리해주는 로직을 수행 할수 있게 된다.

/**
 * Publish the given event to all listeners.
 * @param event the event to publish (may be an {@link ApplicationEvent}
 * or a payload object to be turned into a {@link PayloadApplicationEvent})
 * @param eventType the resolved event type, if known
 * @since 4.2
 */
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
    Assert.notNull(event, "Event must not be null");

    // Decorate event as an ApplicationEvent if necessary
    ApplicationEvent applicationEvent;
    if (event instanceof ApplicationEvent) {
        applicationEvent = (ApplicationEvent) event;
    }
    else {
        applicationEvent = new PayloadApplicationEvent<>(this, event);
        if (eventType == null) {
            eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
        }
    }

    // Multicast right now if possible - or lazily once the multicaster is initialized
    if (this.earlyApplicationEvents != null) {
        this.earlyApplicationEvents.add(applicationEvent);
    }
    else {
        getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
    }

    // Publish event via parent context as well...
    if (this.parent != null) {
        if (this.parent instanceof AbstractApplicationContext) {
            ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
        }
        else {
            this.parent.publishEvent(event);
        }
    }
}

publishEvent(new ContextRefreshedEvent(this));
publishEvent(new ContextClosedEvent(this));
publishEvent(new ContextStartedEvent(this));
publishEvent(new ContextStoppedEvent(this));

 

그리고 여기서 나온 PayloadApplicationEvent는 Payload와 이를 설명하는 타입을 같이 전달하여 처리하는 방식으로 더 궁금한 사람에 한해서만 PayloadApplicationEvent 와 이벤트의 인자로 넘겨야 하는 ResolvableType 을 참고하자.

 

그럼 마지막으로 EventPublicationInterceptor를 보자.

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
    Object retVal = invocation.proceed();

    Assert.state(this.applicationEventClassConstructor != null, "No ApplicationEvent class set");
    ApplicationEvent event = (ApplicationEvent)
            this.applicationEventClassConstructor.newInstance(invocation.getThis());

    Assert.state(this.applicationEventPublisher != null, "No ApplicationEventPublisher available");
    this.applicationEventPublisher.publishEvent(event);

    return retVal;
}

주석을 참고하자면 ApplicationEventPublisher에 등록된 모든 ApplicationListener에 ApplicationEvent를 게시하는 일반적인 인터셉터의 역할이며 .ApplicationEventPublisherAware을 구현하고 있는 구체 클래스이다.

 


 

정리하자면 ApplicationContext의 생명주기에 따라 ApplicationEventPublisher가 생성되어 설정되고 이를 이용해 이벤트를 발행한다.

이게 끝이다.

지금까지 살펴본 바로는 이벤트가 발행 되는 방법은 나름 단순하다고 볼 수 있기에 이벤트를 소비하는 쪽이 더 중요하다는 것을 알 수 있다.

 

다음 게시글에서는 이벤트 리스너에 대해 작성하겠다.

2023.05.07 - [공부 기록] - [SPRING] 스프링이 Event를 다루는 방법 - EventListener

 

[SPRING] 스프링에서 Event를 다루는 방법 - ApplicationEventListener

2023.05.07 - [공부 기록] - [SPRING] 스프링에서 Event를 다루는 방법 - ApplicationEventPublisher [SPRING] 스프링에서 Event를 다루는 방법 - ApplicationEventPublisher 2023.03.08 - [생각 정리] - [GOORM x COMMIT] 기술 부채를

ktae23.tistory.com

 

반응형

댓글