Spring Event
Spring’s eventing mechanism is designed for simple communication between Spring beans within the same application context. However, for more sophisticated enterprise integration needs, the separately maintained Spring Integration project provides complete support for building lightweight, pattern-oriented, event-driven architectures that build upon the well-known Spring programming model.
Spring의 이벤트 메커니즘은 동일한 애플리케이션 컨텍스트 내에서 Spring 빈 간의 간단한 통신을 위해 설계되었습니다. 그러나 보다 정교한 엔터프라이즈 통합이 필요한 경우, 별도로 유지 관리되는 Spring 통합 프로젝트는 잘 알려진 Spring 프로그래밍 모델을 기반으로 하는 경량의 패턴 지향 이벤트 중심 아키텍처를 구축하는 데 완벽한 지원을 제공합니다.
스프링에서는 이벤트를 발행하고, 구독하는 기능을 제공한다.
이는 ApplicationContext에서 제공되는 기능이다.
그런데 실제로 어떻게 작동하는지가 궁금해졌는데, 간단한 구글링으로는 원하는 정보를 찾기 어려웠다.
그래서 공식 문서에서 언급된 클래스들을 중심으로, 실제 코드를 찾아보았다.
ApplicationEventPublisher
이벤트를 발행하기 위해서는, ApplicationEventPublisher
타입의 빈을 주입받아 사용했다.
이 인터페이스는 이벤트를 발행하는 인터페이스로, publishEvent
메서드를 정의한다.
기본 구현체는 ApplicationContext
이다.
ApplicationContext
는 BeanFactory 기능 외에도, 이 인터페이스를 상속하면서 이벤트 발행 기능을 제공하는 것이다.
ApplicationContext
의 추상 구현체인 AbstractApplicationContext
에서 publishEvent
메서드의 구현 내용을 찾아볼 수 있다.
Publish the given event to all listeners. This is the internal delegate that all other publishEvent methods refer to. It is not meant to be called directly but rather serves as a propagation mechanism between application contexts in a hierarchy, potentially overridden in subclasses for a custom propagation arrangement.
protected void publishEvent(Object event, @Nullable ResolvableType typeHint) {
Assert.notNull(event, "Event must not be null");
ResolvableType eventType = null;
// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent applEvent) {
applicationEvent = applEvent;
eventType = typeHint;
}
else {
ResolvableType payloadType = null;
if (typeHint != null && ApplicationEvent.class.isAssignableFrom(typeHint.toClass())) {
eventType = typeHint;
}
else {
payloadType = typeHint;
}
applicationEvent = new PayloadApplicationEvent<>(this, event, payloadType);
}
// Determine event type only once (for multicast and parent publish)
if (eventType == null) {
eventType = ResolvableType.forInstance(applicationEvent);
if (typeHint == null) {
typeHint = eventType;
}
}
// 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) {
abstractApplicationContext.publishEvent(event, typeHint);
}
else {
this.parent.publishEvent(event);
}
}
}
javadoc에서는 아래와 같이 설명하고 있다.
Multicast the given application event to appropriate listeners. If the eventType is null, a default type is built based on the event instance.
이 부분만 보아도, ApplicationEventMulticaster
의 mutlicastEvent
메서드를 호출해 적절한 리스너에 이벤트를 뿌린다는 것을 확인할 수 있다.
// 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);
}
ApplicationEventMulticaster
위에서 이벤트에 대한 처리를 이 객체에게 위임한 것을 확인했다.
Spring은 ApplicationEventMulticaster
인터페이스에는 기본적으로 SimpleApplicationEventMulticaster
구현체를 주입한다.
따라서 SimpleApplicationEventMulticaster
클래스에서 multicastEvent
메서드의 구현 내용을 확인할 수 있었다.
@Override
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : ResolvableType.forInstance(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
String msg = ex.getMessage();
if (msg == null || matchesClassCastMessage(msg, event.getClass()) ||
(event instanceof PayloadApplicationEvent payloadEvent &&
matchesClassCastMessage(msg, payloadEvent.getPayload().getClass()))) {
// Possibly a lambda-defined listener which we could not resolve the generic event type for
// -> let's suppress the exception.
Log loggerToUse = this.lazyLogger;
if (loggerToUse == null) {
loggerToUse = LogFactory.getLog(getClass());
this.lazyLogger = loggerToUse;
}
if (loggerToUse.isTraceEnabled()) {
loggerToUse.trace("Non-matching event type for listener: " + listener, ex);
}
}
else {
throw ex;
}
}
}
ApplicationListener들을 순회하면서 해당하는 리스너일 경우 invoke한다.
만약 이 때 ClassCastException이 발생한다면, try ~ catch 문에서 로그를 남기는 방식으로 처리해준다.
이렇게 해서 적절한 타입의 Event를 받는 listener만 호출되도록 한다.
이 때 적절한 리스너인지는 어떻게 판단할까?
위 코드의 getApplicationListeners()
메서드는 부모 클래스인 AbstractApplicationEventMulticaster
에서 정의하고 있다.
해당 코드를 까보면 내부적으로 아래와 같이 해당 이벤트를 지원하는지, Listener에게 직접 물어봐서 확인하는 것을 알 수 있다.
protected boolean supportsEvent(
ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) {
GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener gal ? gal :
new GenericApplicationListenerAdapter(listener));
return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
}
Listener Bean 들 중 해당 이벤트를 지원하는 것들을 모두 찾아 가져오는 것이다. (전체 코드는 길어서 생략)
ApplicationListenerMethodAdapter
그럼 이제 Listener가 대답하는 부분을 확인하자.
ApplicationListener
또한 다양한 구현체를 가지고 있다. 그 중 EventListener로 지정된 ApplicationListenerMethodAdapter
를 살펴보자.
인터페이스 GenericApplicationListener
을 구현하는 아래와 같은 메서드를 확인할 수 있다.
@Override
public boolean supportsEventType(ResolvableType eventType) {
for (ResolvableType declaredEventType : this.declaredEventTypes) {
if (declaredEventType.isAssignableFrom(eventType)) {
return true;
}
if (PayloadApplicationEvent.class.isAssignableFrom(eventType.toClass())) {
ResolvableType payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric();
if (declaredEventType.isAssignableFrom(payloadType) || eventType.hasUnresolvableGenerics()) {
return true;
}
}
}
return false;
}
아래 내용처럼, EventListener에 지정된 조건이 무엇이 있는지 확인하는 것도 알 수 있다.
private boolean shouldHandle(ApplicationEvent event, @Nullable Object[] args) {
if (args == null) {
return false;
}
String condition = getCondition();
if (StringUtils.hasText(condition)) {
Assert.notNull(this.evaluator, "EventExpressionEvaluator must not be null");
return this.evaluator.condition(
condition, event, this.targetMethod, this.methodKey, args, this.applicationContext);
}
return true;
}
결론
이렇게, 생각보다 간단한 방식으로 구현되어 있음을 확인했다.
Spring MVC에서 요청에 맞는 컨트롤러를 찾기 위한 핸들러 매핑 부분과 유사한 방식으로 구현되어 있다.
핸들러 매핑 구조를 보면서, 매번 순회하는 대신 map으로 관리해서 빠르게 찾으면 안되는 걸까? 라는 생각을 한 적이 있다.
이번에도 잠깐 그런 생각이 들었다.
하지만 ‘조건에 맞는 객체를 찾아오는 로직’에서 ‘조건’에 대한 확장성을 가져가기 위해서
각 요소들이 대답을 할 수 있도록, 추상화해두는 것(supportXXX 와 같은 메서드)이 좋아보인다.
파라미터의 타입이 이벤트 타입과 일치하는지만 확인하려고 한다면, key=타입, value=리스너 로 된 map을 만드는 게 더 좋아보일 수도 있다.
하지만 이는 파라미터 타입 == 이벤트 타입 이라는 하나의 절대적인 조건을 전제로 한다. 유연하지 못하다.
따라서 조건을 만족하는지, 각 객체에게 물어보는 방식으로 구현하는 것이 더 유연한 설계라고 생각된다.
참고 자료
'공부 > Spring' 카테고리의 다른 글
재시도를 위한 Spring Retry @Retryable (0) | 2024.04.27 |
---|---|
Jpa 이벤트를 사용해 일관성 있는 시간 정보 관리하기 (0) | 2023.10.15 |
성능 향상을 위한 Hikari Connection Pool 설정 for MySQL (1) (0) | 2023.09.29 |
Spring Boot의 로깅, 로깅을 왜 할까? (0) | 2023.05.23 |
[문제 해결] @AutoConfigureTestDatabase 설정으로 @JdbcTest에서 원하는 DB 사용하기 (0) | 2023.05.07 |