스프링 프레임워크는 옵저버 패턴(Observer Pattern) 기반으로 이벤트 발행과 구독을 지원한다.
즉, 한쪽에서 이벤트를 publish 하면, 등록된 여러 listener 가 이를 받아 처리할 수 있다.

발행과 구독은 어떤 방식으로 하는지, 더 나아가 내부적으로는 어떻게 동작하는지 코드로 확인해보자.

 

 

 

 

 

스프링에서 이벤트 기능을 쓸 때 따라야 하는 기본 규칙은 다음과 같다:

  • (스프링 4.2 이전) 이벤트 클래스는 ApplicationEvent를 상속해야 함
  • 이벤트 발행은 ApplicationEventPublisher 주입받아 사용
  • 이벤트 구독은 ApplicationListener 인터페이스 구현 or @EventListener 사용

 

 

 

 

 

 

이벤트 발행 (Publish)

  • 이벤트 발행은 ApplicationEventPublisher 인터페이스가 담당한다.
  • 해당 인터페이스의 publishEvent 메소드를 사용하면 된다.
  • 사실 ApplicationContext 자체가 이 인터페이스의 구현체이므로, 어디서든 주입받아 사용할 수 있다.

애플리케이션 컨텍스트는 많은 책임(빈 탐색과 등록, 리소스 처리 등)을 제공하고 있는데,

ApplicationEventPublisher는 인터페이스 분리 원칙에 맞게 이벤트 발행 책임만 처리한다.

 

 

이벤트가 발행되면 애플리케이션 컨텍스트는 해당 이벤트를 구독하는 빈들을 찾아서 notify 해주는데, 

이러한 부분은 내부적으로는 옵저버 패턴을 사용해 구현되어 있다. 

구현되어 있는 코드를 확인해보자.

 

 

 

 

ApplicationEventPublisher

 

 

 

 

 

ApplicationContext

ApplicationContext인터페이스가 인터페이스 분리 원칙에 의해 ApplicationEventPublisher를 상속받고 있고,

 

 

 

 

AbstractApplicationContext

이를 구현하는 추상 클래스 AbstractApplicationContext에 publishEvent가 구현되어 있다.

@Override
public void publishEvent(ApplicationEvent event) {
    publishEvent(event, null);
}

@Override
public void publishEvent(Object event) {
    publishEvent(event, null);

1. ApplicationEvent 타입

2. Object 타입

 

  • 스프링 4.2 이후부터는 그냥 아무 POJO도 이벤트로 발행 가능 → Object 버전 추가됨
  • 결국 둘 다 공통 로직 publishEvent(Object, ResolvableType)으로 위임

 

 

publishEvent 코드를 살펴보자

protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
	Assert.notNull(event, "Event must not be null");

     //이벤트를 ApplicationEvent로 변환
     //만약 이미 ApplicationEvent라면 그대로 사용
     //그냥 일반 객체(POJO)라면 PayloadApplicationEvent로 감싸서 표준 이벤트로 변환
     //→덕분에 publishEvent("hello") 처럼 평범한 객체도 이벤트로 발행 가능
	// 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();
		}
	}

    //초기화 전 이벤트 처리
    //earlyApplicationEvents는 컨텍스트 초기화 도중 발생하는 이벤트들을 잠시 담아두는 버퍼
    //컨텍스트 준비 전이면 리스트에 임시 보관,
    //준비 완료 상태라면 바로 ApplicationEventMulticaster에 위임 → 이벤트 브로드캐스팅 시작
	// 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);
	}

     //부모 컨텍스트 전파
     //스프링은 컨텍스트를 계층 구조(parent-child)로 가질 수 있음
     //부모 컨텍스트가 있다면 이벤트를 부모에게도 전달해서 전파
     //따라서 부모 Bean이 같은 이벤트를 들을 수도 있음
	// 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);
		}
	}
}

 

여기서 결국 호출되는

getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);

도 살펴보자

ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException {
	if (this.applicationEventMulticaster == null) {
		throw new IllegalStateException("ApplicationEventMulticaster not initialized - " +
				"call 'refresh' before multicasting events via the context: " + this);
	}
	return this.applicationEventMulticaster;
}

 

현재 컨텍스트에서 쓰는 이벤트 브로드캐스터를 반환하고, 내부적으로 보통 SimpleApplicationEventMulticaster가 사용된다.

 

 

 

 

 

SimpleApplicationEventMulticaster

SimpleApplicationEventMulticaster의 multicastEvent 로직을 확인해보자

	//편의 메서드.
    	//event만 받으면 내부에서 기본 이벤트 타입(ResolvableType)을 계산해서 두 번째 메서드로 위임
    	@Override
	public void multicastEvent(ApplicationEvent event) {
		multicastEvent(event, resolveDefaultEventType(event));
	}

	@Override
	public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
		
        //이벤트 객체의 실제 타입(ApplicationReadyEvent, MyEvent, …)을 해석
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
		
        //multicaster에 TaskExecutor(스레드 풀)가 설정되어 있으면 → 비동기 실행
        //기본값은 null → 동기 실행 (현재 쓰레드에서 즉시)
        Executor executor = getTaskExecutor();
		
        //등록된 모든 리스너 중에서 이벤트 타입과 매칭되는 리스너만 골라옴
        for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
			//동기 실행: listener.onApplicationEvent(event) 바로 호출
            //비동기 실행: executor(스레드 풀)에 던져서 다른 스레드에서 실행
            if (executor != null) {
				executor.execute(() -> invokeListener(listener, event));
			}
			else {
				invokeListener(listener, event);
			}
		}
	}
    
    
    	private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
		return ResolvableType.forInstance(event);
	}

 

여기서 내부적으로 invokeListener가 호출되고

invoke의 핵심 부분만 보자면 onApplicationEvent를 통해

실제 리스너 코드(@EventListener 붙은 메서드 or ApplicationListener 구현체)가 호출됨

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
    listener.onApplicationEvent(event);
}

 

 

 

 

 

ApplicationListener

ApplicationListener는 다음과 같고, 이를 구현하거나, @EventListener를 통해 구독할 수 있다.

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

   /**
    * Handle an application event.
    * @param event the event to respond to
    */
   void onApplicationEvent(E event);


   /**
    * Create a new {@code ApplicationListener} for the given payload consumer.
    * @param consumer the event payload consumer
    * @param <T> the type of the event payload
    * @return a corresponding {@code ApplicationListener} instance
    * @since 5.3
    * @see PayloadApplicationEvent
    */
   static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
      return event -> consumer.accept(event.getPayload());
   }

}

 

 

 

 

 

따라서 ApplicationEventPublisher를 주입받아서 publishEvent()를 호출하면

결국 AbstractApplicationContext에 정의된 publishEvent(Object event) 메서드가 실행되고, 내부적으로 구독 구현체들의 onApplicationEvent(E event);가 실행되는 것이다.

 

 

실제 다음과 같이 사용하면 된다.

@Service
@RequiredArgsConstructor
public class EventPublisher {

    private final ApplicationEventPublisher publisher;

    public void publish() throws InterruptedException {
        publisher.publishEvent(new ExampleEvent("Event 발행!"));
    }

}

 

 

 

 

 

 

 

 

 

 

 

 

 

이벤트 구독 (Subscribe)

ApplicationListener 구현

스프링 4.2 이전에는 반드시 ApplicationListener<E> 인터페이스를 구현해야 했다.

 

구현 코드

@Component
public class ExampleEventListener implements ApplicationListener<ExampleEvent> {
    @Override
    public void onApplicationEvent(ExampleEvent event) {
        System.out.println("수신: " + event.getMessage());
    }
}


//혹은 람다로 등록도 가능 : 빈으로 등록은 되지 않음. 코드에서 직접 호출
context.addApplicationListener(
    (ExampleEvent event) -> System.out.println("람다로 수신: " + event.getMessage())
);
 

 

 

 

 

 

@EventListener 사용

스프링 4.2부터는 훨씬 간단하게 @EventListener 애노테이션만 붙이면 된다.

@Component
public class ExampleAnnotationListener {
    @EventListener
    public void listen(ExampleEvent event) {
        System.out.println("수신: " + event.getMessage());
    }
}
 
@EventListener는 그냥 애노테이션일 뿐인데, 스프링은 Bean 등록 과정에서 EventListenerMethodProcessor라는 BeanPostProcessor를 사용한다.
 
이 프로세서가
  • 모든 빈의 메서드를 스캔
  • @EventListener 붙은 걸 발견
  • 내부적으로 ApplicationListener 객체로 변환해서 ApplicationEventMulticaster에 등록

그래서 우리가 @EventListener(ApplicationReadyEvent.class)만 달아놔도, multicaster의 listener 목록에 자동으로 들어가 있는 것. ApplicationListener 인터페이스의 타입을 보면 ApplicationEvent를 확장한 제네릭 타입임을 볼 수 있다. 

 
장점
  • 클래스가 ApplicationListener 안 구현해도 됨 (메서드만 있으면 됨)
  • 메서드 단위로 리스너를 쉽게 추가 가능
  • 같은 클래스 안에 여러 이벤트 리스너를 모아둘 수 있음
  • @TransactionalEventListener 같은 확장 기능도 애노테이션 기반이라 자연스럽게 연결됨

 

 

 

 

 

@TransactionalEventListener

스프링 이벤트 리스너(@EventListener)의 확장판

단순히 이벤트가 발행되었다고 바로 실행되지 않고, 트랜잭션의 상태(Commit, Rollback 등)에 따라 실행 시점이 달라짐

 

@Component
public class OrderEventListener {

    // 트랜잭션 커밋 이후 실행
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderPlaced(OrderPlacedEvent event) {
        System.out.println("주문 완료 → 이메일 발송");
    }

    // 롤백 이후 실행
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void handleOrderFailed(OrderFailedEvent event) {
        System.out.println("주문 실패 → 보상 처리");
    }

    // 커밋 직전에 실행
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void beforeCommit(OrderPlacedEvent event) {
        System.out.println("주문 직전 → 재고 수량 확인");
    }

    // 트랜잭션 종료 후 (커밋/롤백 무관)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void afterCompletion(OrderPlacedEvent event) {
        System.out.println("트랜잭션 종료 후 로그 기록");
    }
}

fallbackExecution (기본값: false)

  • 이벤트가 트랜잭션 밖에서 발행되면 기본적으로는 실행 안 됨
  • true로 설정하면, 트랜잭션이 없어도 그냥 실행하게 할 수 있음

classes

  • 구독할 이벤트 타입 지정 (일반 @EventListener처럼 동작)
  • 메서드 파라미터 방식을 더 많이씀

 

언제 쓰면 좋은가?

  • DB 상태와 맞물려야 하는 이벤트 처리에 적합
  • AFTER_COMMIT: 결제가 DB에 정상 저장된 후 알림/메시지 큐 발행
  • AFTER_ROLLBACK: 실패 케이스에만 보정 로직 수행
  • BEFORE_COMMIT: 데이터 커밋 전에 반드시 맞춰야 하는 캐시/인덱스 작업
  • AFTER_COMPLETION: 성공/실패 무관한 cleanup

 

 

 

 

 

 

 

흐름 총정리

  1. applicationEventPublisher.publishEvent(new MyEvent(...))
  2. (POJO라면 PayloadApplicationEvent로 감쌈)
  3. AbstractApplicationContext.publishEvent() → ApplicationEventMulticaster.multicastEvent()
  4. SimpleApplicationEventMulticaster → 적절한 리스너 탐색
  5. 각 리스너에 대해 invokeListener(listener, event) 실행
  6. 결과:
    • ApplicationListener 구현체 → onApplicationEvent() 호출
    • @EventListener 메서드 → ApplicationListenerMethodAdapter.invoke() 호출

 

 

 

 

 

 

EX)

 

스프링 부트도 자체적으로 다양한 애플리케이션 이벤트를 발행한다.
예를 들어 애플리케이션 실행 시 마지막 단계에 ApplicationReadyEvent가 발생한다.

 

그럼 내부적으로 아래와 같은 단계를 거쳐 구독한 메서드를 실행한다.

 

SpringApplication.run()
      ↓
publishEvent(new ApplicationReadyEvent)
      ↓
ApplicationEventMulticaster.multicastEvent()
      ↓
getApplicationListeners() : 이벤트 타입 맞는 리스너 목록 필터링
   ↓
invokeListener(listener, event)
   ↓
listener.onApplicationEvent(event) 실행
      ↓
@EventListener(ApplicationReadyEvent.class) 메서드 실행

 
 
 
 
 
 
 

 

 

참고

https://mangkyu.tistory.com/292

복사했습니다!