반응형
스프링 이벤트란?
- 스프링 이벤트는 스프링 프레임워크를 사용할 때 빈(Bean) 간 통신을 위해 설계되었다. 그러나 보다 정교한 엔터프라이즈 통합 요구 사항을 위해 별도로 유지 관리되는 Spring Integration 프로젝트는 잘 알려진 Spring 프로그래밍 모델을 기반으로 하는 경량의 패턴 지향 이벤트 중심 아키텍처 구축을 위한 완벽한 지원을 제공한다.
- 이벤트를 발행(Publish)하고 이벤트를 수신 또는 구독하여 소비(Listen/Subscribe)하는 기능을 제공한다.
ApplicationListener
인터페이스를 구현하는 Bean이 컨텍스트에 배포되면ApplicationEvent
가ApplicationContext
에 이벤트가 발행될 때마다 해당 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 클래스를 확장할 필요가 없다.
스프링 커스텀 이벤트
- 사용자 정의 이벤트를 발행할 수 있다. ApplicationEventPublisher의 publishEvent() 메서드를 사용해서 이벤트를 발행한다.
- 이벤트 리스너는 원하는 만큼 등록할 수 있지만 기본적으로 이벤트 리스너는 이벤트를 동기식으로 수신한다.
- 이는 모든 리스너가 이벤트 처리를 완료할 때까지
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
를 구현하여 런타임 환경이 제공하는 것 이상으로 프레임워크를 가이드할 수 있다. 다음 이벤트는 이를 수행하는 방법을 보여준다.
- 예)
이는 ApplicationEvent뿐만 아니라 이벤트로 보내는 임의의 개체에도 적용된다.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())); } }
임의의 타입의 이벤트를 발생할 수 있도록 유연성을 가진다.
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부터@TransactionalEventListener
는PlatformTransactionManager
가 관리하는 스레드 바인딩 트랜잭션과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
를 사용하지 말 것.
참고
반응형
'Java > Spring Framework' 카테고리의 다른 글
[스프링] 스케줄링, 비동기 추상화(@Async, @Scheduled) (0) | 2024.03.13 |
---|---|
[스프링 핵심 원리 - 기본편] 웹 스코프 (0) | 2021.09.04 |
[스프링 핵심 원리 - 기본편] 빈 스코프 (0) | 2021.08.31 |
[스프링 핵심 원리 - 기본편] 빈 생명주기 콜백 (0) | 2021.08.30 |
[스프링 핵심 원리 - 기본편] 같은 타입의 빈 여러 개 조회 문제 (0) | 2021.08.29 |