본문 바로가기
Spring/Core

[Spring] EventListener

by 기몬식 2025. 7. 10.

Spring Event

스프링 애플리케이션을 실행할 때 ApplicationContext가 초기화되고 서버가 동작하게 되는 과정에서 내부적으로는 굉장히 많은 이벤트가 순차적으로 발생합니다. 스프링 프레임워크는 기본적으로 다음과 같은 이벤트들을 발행함으로써 자체적으로 ApplicationContext의 라이프 사이클을 관리합니다.


  • ContextRefreshedEvent: ApplicationContext가 초기화되거나 리프레시된 후(보통 context.refresh() 또는 애플리케이션 시작 시 발생)
  • ContextStartedEvent: context.start() 가 호출될 때 발생
  • ContextStoppedEvent: context.stop() 이 호출될 때 발생
  • ContextClosedEvent: context.close() 또는 JVM 종료 시 발생(자원 정리 필요할 때 사용)

이러한 이벤트 외에도 스프링 부트에서는 다음과 같은 부트 전용 애플리케이션 초기화 이벤트들을 추가로 제공합니다.

  • ApplicationStartingEvent: JVM 실행 직후, 가장 처음 발생
  • ApplicationEnvironmentPreparedEvent: Environment 가 준비된 후(프로파일 및 프로퍼티 확인 가능)
  • ApplicationContextInitializedEvent: ApplicationContext가 생성된 직후(Bean 등록 전)
  • ApplicationPreparedEvent: ApplicationContext에 Bean 등록 완료 후(context.refresh()는 아직 호출 전)
  • ApplicationStartedEvent: context.refresh() 직후(대부분의 Bean 초기화 완료 시점)
  • ApplicationReadyEvent: 모든 애플리케이션 초기화 및 CommandLineRunner, ApplicationRunner까지 실행 완료 후
  • ApplicationFailedEvent: 애플리케이션 부팅 중 Exception으로 실패했을 때 발생

이처럼 부트 초기화부터 서버가 가동되는 과정이 이벤트로 명확히 구분되어 있기 때문에 각각의 시점을 활용하여 초기화 커스터마이징, 외부 연동, 설정 검증 등 다양한 작업들을 안전하게 수행할 수 있습니다. 모든 이벤트는 내부적으로 ApplicationEventPublisher를 통해 발행되며 이를 기반으로 스프링은 느슨하게 결합된 구조를 유지한 채 복잡한 초기화 흐름을 제어합니다.


하지만 이 구조는 우리가 직접 사용할 수 있는 확장 포인트이기도 합니다. `ApplicationListener` 인터페이스를 이용한 이벤트 발행은 단순히 `ApplicationContext` 라이프사이클 관리에만 국한되지 않고 개발자들이 직접 정의한 도메인 로직에도 확장해서 사용할 수 있도록 인터페이스와 어노테이션 기반의 추상화를 제공해줍니다. 스프링은 `@EventListener`라는 간단한 어노테이션 하나만으로 이 이벤트 시스템을 우리 코드에서 사용할 수 있게 해줍니다.

@EventListener

스프링은 오래전부터 이벤트 기반 프로그래밍을 지원해왔지만 초기에는 지금처럼 직관적이고 간단한 방식이 아니었습니다. 스프링 프레임워크가 발전하면서 이벤트 리스닝 방식도 점차 개선되어 왔고 현재는 @EventListener라는 어노테이션 기반의 간결한 방식으로 자리잡았습니다.


@Component
class SampleService(
    private val eventPublisher: ApplicationEventPublisher
) {
  fun doSomething(id: Long) {
      eventPublisher.publishEvent(SampleEvent(id))
  }
}

@Component
class SampleEventListener : ApplicationListener<SampleEventEvent> {
  override fun onApplicationEvent(event: SampleEventEvent) {
      println("이벤트 수신: ${event.orderId}")
  }
}

Spring 3.0 이전에서는 ApplicationEventPublisher 를 직접 DI 받아 이벤트를 발행하고 ApplicationListener 를 직접 구현한 리스너를 통해 이벤트를 구독하는 방식으로 이루어졌습니다. 하지만 스프링 4.2부터 @EventListener 어노테이션이 도입되면서 어노테이션 기반으로 메서드 단위에서 간결하게 이벤트를 처리할 수 있게 되었습니다.


@Component
class SampleEventHandler {

    @EventListener
    fun handleOrderCreated(event: SampleEvent) {
        // 이벤트 처리 로직
    }
}

결과적으로 @EventListener는 단순히 문법적 편의성을 제공하는 수준이 아니라 이벤트 기반 구조의 확장성과 유지보수성을 고려했을 때도 현재 가장 권장되는 방식입니다.

동작 방식

스프링의 @EventListener는 동기적 메소드 호출에 가깝습니다. 내부적으로는 이벤트를 발행하면 해당 이벤트를 처리할 수 있는 리스너 메소드를 찾아 리플랙션을 통해 직접 invoke() 메소드가 호출됩니다. 즉 스프링에서 기본적으로 여러 기능을 제공하는 방식에 일관되면서 단순하고 강력합니다.

동작 원리를 살펴보기 전에 먼저 다음과 같은 예제를 작성하겠습니다.


@Component
class SampleEventHandler {

  @EventListener
  fun handle(event: SampleEvent) {
      println("message: ${event.message}")
  }

}

data class SampleEvent(val message: String)

그 후 디버깅을 위한 이벤트를 실행할 테스트 코드를 작성합니다.



@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@SpringBootTest
class SampleEventHandlerTest(
    private val applicationEventPublisher: ApplicationEventPublisher,
) {

  @Test
  fun testPublishEvent() {
      applicationEventPublisher.publishEvent(SampleEvent("hello world!"))
  }

}

가장 먼저 applicationEventPublisher.publishEvent() 를 호출하게 되면 실제로는 AbstractApplicationContext.publishEvent() 추상 메소드를 호출함로써 발급한 이벤트에 관한 클래스 정보를 파싱합니다.


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

그 후 ApplicationEventMulticaster 를 통해서 이벤트를 여러 리스너에게 순차적으로 전달합니다. 즉 전달받은 이벤트 클래스 정보를 전달하면서 해당 이벤트를 처리할 수 있는 리스너를 찾아서 하나씩 호출하는 의미입니다.



이벤트를 처리할 수 있는 리스너를 찾으러갑니다.



먼저 리스너를 직접 찾기 전에 주어진 이벤트 타입과 소스 타입을 처리할 수 있는 캐싱된 리스너가 있는지 찾음과 동시에 해당 이벤트를 캐싱합니다.
현재 캐싱된 리스너가 없기 때문에 retrieveApplicationListeners()를 통해 직접 리스너를 찾으러 갑니다.



beanFactory 에서 ApplicationListener 타입을 가진 빈을 찾아서 조회합니다. @EventListener는 내부적으로 프록시 기반 ApplicationListenerMethodAdapter로 래핑되기 떄문에 컴파일 시점에 ApplicationListener 를 구현하지 않지만 동일한 클래스 타입으로 인식되어 ApplicationContext 에 등록되기 때문에 beanFactory에서 반환됩니다.



최종적으로 반환된 리스너의 함수를 invoke() 합니다.


이번 글에서는 스프링의 이벤트 시스템 관련 그리고 @EventListener 기반으로 어떤 방식으로 이벤트 리스너가 등록되고 실행되는지까지 살펴보았습니다.
단순한 어노테이션으로 시작된 이벤트 리스너가 어떤 흐름을 따라 실행되는지 디버깅하며 살펴봤고 그 내부에서 스프링의 bean과 컬렉션 형태를 가짐으로써 갖는 구조적인 유연성과 확장성을 더 깊이 체감할 수 있습니다.


오탈자 및 오류 내용을 댓글 또는 메일로 알려주시면, 검토 후 조치하겠습니다.

'Spring > Core' 카테고리의 다른 글

[Spring] @TransactionalEventListener  (2) 2025.07.23
[Spring] @Transactional 속성  (1) 2023.12.24
[Spring] Spring AOP 동작 방식  (0) 2023.08.30
[Spring] Spring AOP JDK Dynamic & CGLIB Proxy 생성 방식  (0) 2023.08.28
[Spring] Spring AOP란?  (0) 2023.08.27