반응형

 

스프링 이벤트란?

  • 스프링 이벤트는 스프링 프레임워크를 사용할 때 빈(Bean) 간 통신을 위해 설계되었다. 그러나 보다 정교한 엔터프라이즈 통합 요구 사항을 위해 별도로 유지 관리되는 Spring Integration 프로젝트는 잘 알려진 Spring 프로그래밍 모델을 기반으로 하는 경량의 패턴 지향 이벤트 중심 아키텍처 구축을 위한 완벽한 지원을 제공한다.
  • 이벤트를 발행(Publish)하고 이벤트를 수신 또는 구독하여 소비(Listen/Subscribe)하는 기능을 제공한다.
  • ApplicationListener 인터페이스를 구현하는 Bean이 컨텍스트에 배포되면 ApplicationEventApplicationContext에 이벤트가 발행될 때마다 해당 Bean에 알림이 전달된다.
  • 본질적으로 이것은 표준 Observer 디자인 패턴이다.

 

 

스프링에서 기본으로 내장된 이벤트

  • ContextRefreshedEvent
    • ApplicationContext가 초기화되거나 새로고쳐질 때 이벤트 발행
    • “초기화”는 모든 Bean이 로드되고, post-processor Bean들이 감지 및 활성화되고, 싱글톤이 사전 인스턴스화되고, ApplicationContext 객체를 사용할 준비가 되었음을 의미한다.
  • ContextStartedEvent
    • ConfigurableApplicationContext 인터페이스의 start() 메서드를 사용하여 ApplicationContext시작될 때 이벤트 발행
    • "시작됨"은 모든 Lifecycle Bean이 명시적인 시작 신호를 수신함을 의미한다.
  • ContextStoppedEvent
    • ConfigurableApplicationContext 인터페이스의 stop() 메서드를 사용하여 ApplicationContext중지되면 이벤트 발행
    • "중지됨"은 모든 Lifecycle Bean이 명시적인 중지 신호를 수신함을 의미한다. 중지된 컨텍스트는 start() 호출을 통해 다시 시작될 수 있다.
  • ContextClosedEvent
    • ConfigurableApplicationContext 인터페이스의 close() 메소드를 사용하거나 JVM 종료 hook을 통해 ApplicationContext닫힐 때 이벤트 발행
    • "닫힘"은 모든 싱글톤 Bean이 삭제됨을 의미한다. 컨텍스트가 닫히면 수명이 다해 새로 고치거나 다시 시작할 수 없다.
  • RequestHandledEvent
    • HTTP 요청이 서비스되었음을 모든 Bean에 알리는 웹 관련 이벤트이다.
    • 이 이벤트는 요청이 완료된 후 게시된다. 이 이벤트는 Spring의 DispatcherServlet을 사용하는 웹 애플리케이션에만 적용 가능하다.
  • ServletRequestHandledEvent
    • 서블릿별 컨텍스트 정보를 추가하는 RequestHandledEvent의 하위 클래스입니다.

 

Spring Framework 4.2 이전 버전을 사용하는 경우 이벤트 클래스는 ApplicationEvent를 확장해야 하지만 4.2 버전부터 이벤트 클래스는 더 이상  ApplicationEvent  클래스를 확장할 필요가 없다.

 

스프링 커스텀 이벤트

  • 사용자 정의 이벤트를 발행할 수 있다. ApplicationEventPublisherpublishEvent() 메서드를 사용해서 이벤트를 발행한다.
  • 이벤트 리스너는 원하는 만큼 등록할 수 있지만 기본적으로 이벤트 리스너는 이벤트를 동기식으로 수신한다.
    • 이는 모든 리스너가 이벤트 처리를 완료할 때까지 publishEvent() 메서드가 차단됨을 의미한다.
    • 이 동기식 및 단일 스레드 접근 방식의 한 가지 장점은 리스너가 이벤트를 수신할 때 트랜잭션 컨텍스트를 사용할 수 있는 경우 발행자의 트랜잭션 컨텍스트 내에서 작동한다는 점이다.
  • 이벤트 리스너 전략을 기본적으로 비동기식으로 동작하도록 변경하려면 Spring의 ApplicationEventMulticaster 인터페이스를 bean으로 구현해야 한다. 사용자 정의 bean의 이름은 applicationEventMulticaster 으로 설정하고 SimpleApplicationEventMulticaster 을 구현체로 사용한다.
  • 예) 이벤트 발행
    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); //이벤트 발행
        }
    }

 

주석 기반 이벤트 리스너(Annotation-based Event Listeners)

@EventListener 주석을 사용하여 관리되는 Bean의 모든 메서드에 이벤트 리스너를 등록할 수 있다.

  • 예) 
    public class BlockedListNotifier {
    
    	private String notificationAddress;
    
    	public void setNotificationAddress(String notificationAddress) {
    		this.notificationAddress = notificationAddress;
    	}
    
    	@EventListener
    	public void processBlockedListEvent(BlockedListEvent event) {
    		// notify appropriate parties via notificationAddress...
    	}
    }

 

메소드가 여러 이벤트를 수신해야 하거나 매개변수 없이 정의하려는 경우 이벤트 유형을 주석 자체에 지정할 수도 있다.

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

 

특정 이벤트에 대한 메소드를 실제로 호출하기 위해 일치해야 하는 SpEL 표현식을 정의하는 주석의 조건 속성을 사용하여 추가적인 런타임 필터링을 추가하는 것도 가능하다.

  • 다음 예에서는 이벤트의 콘텐츠 속성이 my-event와 동일한 경우에만 호출되도록 작성할 수 있는 방법을 보여준다.
  • 예)
    @EventListener(condition = "#blEvent.content == 'my-event'")
    public void processBlockedListEvent(BlockedListEvent blEvent) {
    	// notify appropriate parties via notificationAddress...
    }
  • 각 SpEL 표현식은 전용 컨텍스트에 대해 평가된다. 조건부 이벤트 처리에 사용할 수 있도록 컨텍스트에 제공되는 항목이 나열되어 있다.

 

다른 이벤트를 처리한 결과로 이벤트를 게시해야 하는 경우, 다음 예제와 같이 게시되어야 하는 이벤트를 반환하도록 메서드 시그니처를 변경할 수 있다.

  • 비동기 리스너에는 이 기능이 지원되지 않는다.
  • handlerBlockedListEvent() 메서드는 처리하는 모든 BlockedListEvent에 대해 새 ListUpdateEvent를 발행한다. 여러 이벤트를 발행해야 하는 경우 대신 컬렉션이나 이벤트 배열을 반환할 수도 있다.
  • 예)
    @EventListener
    public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    	// notify appropriate parties via notificationAddress and
    	// then publish a ListUpdateEvent...
    }

 

비동기 리스너(Asynchronous Listeners)

특정 리스너가 이벤트를 비동기적으로 처리하도록 하려면 일반 @Async 지원을 재사용할 수 있다. 다음 예에서는 이를 수행하는 방법을 보여준다.

  • 예)
    @EventListener
    @Async
    public void processBlockedListEvent(BlockedListEvent event) {
    	// BlockedListEvent is processed in a separate thread
    }

 

비동기 이벤트를 사용할 때의 제한 사항

  • 비동기 이벤트 리스너가 예외를 발생시키는 경우 호출자에게 전파되지 않는다.
  • 비동기 이벤트 리스너 메서드는 값을 반환하여 후속 이벤트를 게시할 수 없다.
    • 처리 결과로 다른 이벤트를 게시해야 하는 경우 ApplicationEventPublisher를 삽입하여 이벤트를 수동으로 게시해야한다.
  • ThreadLocal 및 로깅 컨텍스트는 이벤트 처리를 위해 기본적으로 전파되지 않는다.

 

 

이벤트 리스너 우선순위(Ordering Listeners)

하나의 리스너를 다른 리스너보다 먼저 호출해야 하는 경우 다음 예제와 같이 @Order 주석을 메서드 선언에 추가할 수 있다.

  • 예)
    @EventListener
    @Order(42)
    public void processBlockedListEvent(BlockedListEvent event) {
    	// notify appropriate parties via notificationAddress...
    }

 

제네릭 이벤트(Generic Events)

  • 제네릭을 사용하여 이벤트 구조를 추가로 정의할 수도 있다
  • 예) EntityCreatedEvent<T> 를 정의해서 사용한다. 여기서 T는 생성된 실제 엔터티의 유형이다. 예를 들어 다음 리스너 정의를 생성하여 Person에 대해 EntityCreatedEvent만 수신할 수 있다.
    @EventListener
    public void onPersonCreated(EntityCreatedEvent<Person> event) {
    	// ...
    }
  • 타입 이레이져로 인해 이는 발생한 이벤트가 이벤트 리스너가 필터링하는 일반 매개변수를 확인하는 경우에만 작동한다.
    • 즉, class PersonCreatedEvent extends EntityCreatedEvent<Person> { … } PersonCreatedEvent 클래스 같은 것이다.

 

특정 상황에서는 모든 이벤트가 동일한 구조를 따르는 경우 매우 지루해질 수 있다. (앞의 예에 있는 이벤트의 경우에도 마찬가지이다.)

  • 이러한 경우 ResolvableTypeProvider를 구현하여 런타임 환경이 제공하는 것 이상으로 프레임워크를 가이드할 수 있다. 다음 이벤트는 이를 수행하는 방법을 보여준다.
  • 예)
    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뿐만 아니라 이벤트로 보내는 임의의 개체에도 적용된다.
    임의의 타입의 이벤트를 발생할 수 있도록 유연성을 가진다.

 

 

ApplicationEventMulticaster

  • 실제 멀티캐스팅은 런타임 시 컨텍스트 전체의 ApplicationEventMulticaster를 통해 발생한다.
  • 기본적으로 이는 호출자 스레드에서 동기 이벤트 게시가 포함된 SimpleApplicationEventMulticaster 이다.
  • 이는 "applicationEventMulticaster" 빈 정의를 통해 대체/맞춤화될 수 있다.
  • 예) 모든 이벤트를 비동기식으로 처리하거나 리스너 예외를 처리하기 위해서 applicationEventMulticaster 빈 재정의
    @Bean
    ApplicationEventMulticaster applicationEventMulticaster() {
    	SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
    	multicaster.setTaskExecutor(...); //쓰레드 풀
    	multicaster.setErrorHandler(...); //에러 핸들러
    	return multicaster;
    }

 

ApplicationEventMulticaster 를 재정의하면 이벤트의 전체적인 전략이 수정된다.
일부만 비동기로 사용하고 싶다면
@Async 를 사용하는 것이 좋아보인다.

 

트랜잭션 바운드 이벤트(Transaction-Bound Events)

  • 스프링 4.2부터 이벤트 리스너는 트랜잭션 단계에 바인딩될 수 있다.
  • 예) 트랜잭션이 성공적으로 완료되었을 때 이벤트를 처리
    • 현재 트랜잭션의 결과가 실제로 리스너에게 중요한 경우, 이벤트를 더 유연하게 사용할 수 있다.
  • @EventListener 주석을 사용하여 일반 이벤트 리스너를 등록할 수 있다. 트랜잭션에 바인딩해야 하는 경우에는 @TransactionalEventListener를 사용해야 한다.
    • 기본적으로 리스너가 트랜잭션의 커밋 단계에 바인딩된다.
  • 실행 중인 트랜잭션이 없으면 필수 의미 체계를 준수할 수 없으므로 리스너가 전혀 호출되지 않는다. 그러나 주석의 fallbackExecution 속성을 true로 설정하여 해당 동작을 재정의할 수 있다.

 

  • 예) 구성 요소가 주문 생성 이벤트를 발행하고 해당 이벤트가 발행된 트랜잭션이 성공적으로 커밋된 후에만 해당 이벤트를 처리해야 하는 리스너를 정의하는 상황
    @Component
    public class MyComponent {
    
    	@TransactionalEventListener
    	public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
    		// ...
    	}
    }

 

스프링 6.1부터 @TransactionalEventListenerPlatformTransactionManager가 관리하는 스레드 바인딩 트랜잭션과 ReactiveTransactionManager가 관리하는 반응형 트랜잭션과 함께 작동할 수 있다.
전자의 경우 리스너는 현재 스레드 바인딩된 트랜잭션을 볼 수 있다.
후자는 스레드로컬 변수 대신 Reactor 컨텍스트를 사용하므로 트랜잭션 컨텍스트는 게시된 이벤트 인스턴스에 이벤트 소스로 포함되어야 한다.
자세한 내용은 TransactionalEventPublisher javadoc 참조

 

@TransactionalEventListener

  • AFTER_COMMIT(기본값) : 성공적으로 완료된 경우 이벤트를 발생시키는 데 사용된다.
  • AFTER_ROLLBACK : 트랜잭션이 롤백된 경우
  • AFTER_COMPLETION : 트랜잭션이 완료된 경우(AFTER_COMMIT 및 AFTER_ROLLBACK 의 별칭)
  • BEFORE_COMMIT : 트랜잭션 커밋 직전에 이벤트를 발생시키는 데 사용된다

 

  • 예) 트랜잭션 커밋 직전에 이벤트 발행
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void handleCustom(CustomSpringEvent event) {
        System.out.println("Handling event inside a transaction BEFORE COMMIT.");
    }

 

  • 주의사항
    • AFTER_COMMIT phase 에서 JPA Entity에 수정해도 반영 안된다.
      • 이미 트랜잭션은 커밋한 상태이고 트랜잭션 자체는 기존 것을 유지하고 있다. 기존 JPA Entity 뿐만 아니라 신규 트랜잭션도 실행되지 않는다.
    • @Async를 걸지 않으면 이벤트 처리를 마칠 때 까지 DB 커넥션을 놓치 않게 된다.
      • @Async 로 빨리 응답을 주고 기존 DB 커넥션도 반환하는게 보통을 더 좋을 것으로 보인다.
    • BEFORE_COMMIT phase 에서 뭔가를 할 때는 @Async를 사용하지 말 것.

 

 

 

참고

 

반응형

+ Recent posts