- SpringBoot Events
- SpringBoot @DomainEvent, AbstractAggregateRoot 이용한 Domain Event 발행
이번 장에서는 SpringBoot framework 내에서 이벤트를 발행하고 사용하는 방법에 대해서 살펴보겠습니다.
SpringBoot Events
이벤트는 스프링 프레임워크에서 간과되는 기능 중 하나이지만 유용한 기능입니다. 이벤트 발행(event publishing)은 ApplicationContext가 제공하는 기능 중 하나로 이미 표준화된 방법을 제공합니다.
- Event는 ApplicationEvent를 확장해서 사용합니다.
- 이벤트 게시자(Event Publisher)는 ApplicationEventPublisher에 이벤트를 발행합니다.
- 이벤트를 소비하는 리스너(Event Listener)는 ApplicationListener 인터페이스를 구현하여 이벤트를 받습니다.
Project 생성
https://start.spring.io/에서 프로젝트를 하나 생성합니다.
build.gradle
plugins { id 'org.springframework.boot' version '2.4.3' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.daddyprogrammer' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } test { useJUnitPlatform() }
Event 생성
ApplicationEvent를 확장한 이벤트를 아래와 같이 하나 생성합니다.
package com.daddyprogrammer.springevents.event; import org.springframework.context.ApplicationEvent; public class CustomEvent extends ApplicationEvent { private String message; public CustomEvent(Object source, String message) { super(source); this.message = message; } public String getMessage() { return message; } }
Event Publisher 생성
위에서 작성한 이벤트를 발행할 수 있는 게시자를 아래와 같이 생성합니다. publish 메서드에서 ApplicationEventPublisher로 이벤트를 발행합니다.
package com.daddyprogrammer.springevents.event; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; @RequiredArgsConstructor @Component public class CustomEventPublisher { private final ApplicationEventPublisher applicationEventPublisher; public void publish(final String message) { System.out.println("Publishing custom event. "); CustomEvent customSpringEvent = new CustomEvent(this, message); applicationEventPublisher.publishEvent(customSpringEvent); } }
Event Listener 생성
ApplicationEventPublisher에 발행된 이벤트는 ApplicationListener를 확장하여 구현할 수 있습니다.
package com.daddyprogrammer.springevents.event; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @Component public class CustomEventListener implements ApplicationListener<CustomEvent> { @Override public void onApplicationEvent(CustomEvent event) { System.out.println("Received spring custom event - " + event.getMessage()); } }
이벤트 발행 및 소비 테스트
아래와 같이 Controller를 하나 만들어 message가 발행되고 소비되는지 확인해볼 수 있습니다.
package com.daddyprogrammer.springevents.controller; import com.daddyprogrammer.springevents.event.CustomEventPublisher; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RestController public class EventController { private final CustomEventPublisher customEventPublisher; @GetMapping("/event") public String event(@RequestParam String message) { customEventPublisher.publish(message); return "finished"; } }
웹서버를 실행하고 브라우저에서 메시지를 발송하면 다음과 같이 발행된 메시지가 소비되는 것을 확인할 수 있습니다.
localhost:8080/event?message=hello
Publishing custom event. Received spring custom event - hello
비동기로 이벤트 실행하기
만약 이벤트를 처리하는데 오래 걸리면 응답이 지연되기 때문에 사용자가 오래 기다려야 하며 이런 상황에서는 이벤트를 비동기로 처리하는 것이 좋습니다.
예) 회원가입 후 가입 완료 메일 발송을 비동기 이벤트로 처리하면 회원이 메일 발송이 완료될 때까지 응답을 기다리지 않아도 됩니다.
테스트를 위해 다음과 같이 이벤트 처리 시 지연을 발생시킵니다.
@Component public class CustomEventListener implements ApplicationListener<CustomEvent> { @Override public void onApplicationEvent(CustomEvent event) { try { Thread.sleep(3000); System.out.println("Received spring custom event - " + event.getMessage()); } catch (Exception e) { e.printStackTrace(); } } }
다시 서버를 실행하고 요청을 보내면 3초 후에 응답이 오는 것을 확인할 수 있습니다.
Async 설정 추가
다음과 같이 이벤트가 비동기로 실행될 수 있도록 설정을 추가합니다. 설정 추가 후에 다시 요청을 보내면 응답은 바로 오지만 이벤트는 백그라운드에서 따로 처리되는 것을 확인할 수 있습니다.
@Configuration public class AsyncEventConfig { @Bean(name = "applicationEventMulticaster") public ApplicationEventMulticaster simpleApplicationEventMulticaster() { SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster(); eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor()); return eventMulticaster; } }
Annotation 기반 EventListener
리스너 구현을 위해 일일이 ApplicationListener를 구현할 필요 없이 @EventListener 어노테이션을 통해 관리 빈의 모든 공용 메서드에 리스너를 등록할 수 있습니다. 위에서 작성한 CustomEventListener는 다음과 같이 대체할 수 있습니다.
package com.daddyprogrammer.springevents.event; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component public class AnnotaionListener { @EventListener public void handleEvent(CustomEvent event) { System.out.println("Received spring custom event by annotation listener - " + event.getMessage()); } }
Annotation 기반 비동기 처리
위에서 설정한 전역 비동기 처리도 AsyncEventConfig를 등록하지 않고 @Async를 이용하여 처리할 수 있습니다. @Async를 활성화하려면 Application에 @EnableAsync를 추가하면 됩니다.
@EnableAsync @SpringBootApplication public class SpringEventsApplication { public static void main(String[] args) { SpringApplication.run(SpringEventsApplication.class, args); } }
이제 EventListener에 @Async를 적용하면 비동기로 이벤트가 처리됩니다.
@Component public class AnnotaionListener { @Async @EventListener public void handleEvent(CustomEvent event) { try { Thread.sleep(3000); System.out.println("Received spring custom event by annotation listener - " + event.getMessage()); } catch (Exception e) { e.printStackTrace(); } } }
Generic Event
이벤트 형태를 Generic으로 선언하여 다양한 형태의 이벤트를 처리할 수 있습니다. 또한 Spring Expression Language (SpEL)을 사용하여 특정한 상태의 이벤트만 처리할 수도 있습니다.
GenericEvent 생성
package com.daddyprogrammer.springevents.event; import lombok.Getter; @Getter public class GenericEvent <T> { private T result; protected boolean success; public GenericEvent(T result, boolean success) { this.result = result; this.success = success; } }
GenericEventPublisher 생성
package com.daddyprogrammer.springevents.event; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; @RequiredArgsConstructor @Component public class GenericEventPublisher<T> { private final ApplicationEventPublisher applicationEventPublisher; public void publish(final T message, final boolean success) { System.out.println("Publishing generic event. "); GenericEvent<T> genericEvent = new GenericEvent<>(message, success); applicationEventPublisher.publishEvent(genericEvent); } }
GenericEventListener 생성
listener에 조건(condition)을 설정하여 event.success == true일 때만 처리할 수 있습니다.
package com.daddyprogrammer.springevents.event; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component public class GenericEventListener { @EventListener(condition = "#event.success") public void handleEvent(GenericEvent event) { System.out.println("Received spring generic event by annotation listener - " + event.getResult()); } }
EventController에 generic event 테스트를 위한 endpoint 추가
@RequiredArgsConstructor @RestController public class EventController { private final CustomEventPublisher customEventPublisher; private final GenericEventPublisher<String> genericEventPublisher; @GetMapping("/event") public String event(@RequestParam String message) { customEventPublisher.publish(message); return "finished"; } @GetMapping("/event/generic") public String event(@RequestParam String message, @RequestParam boolean success) { genericEventPublisher.publish(message, success); return "finished"; } }
서버를 실행하고 요청을 보내면 success 변수의 값이 따라 이벤트가 처리되거나 처리되지 않는 것을 확인할 수 있습니다.
http://localhost:8080/event/generic?message=hello&success=true
Publishing generic event. Received spring generic event by annotation listener - hello
http://localhost:8080/event/generic?message=hello&success=false
Publishing generic event.
Transaction 바운더리에서의 이벤트 처리
@EventListener의 확장인 @TransactionalEventListener를 사용하면 트랜잭션 상태에 따른 이벤트 처리가 가능합니다. phase를 설정하지 않으면 트랜잭션이 성공적으로 완료되었을 때 이벤트가 실행됩니다.
- AFTER_COMMIT (default) – 트랜잭션이 성공했을때 실행
- AFTER_ROLLBACK – 트랜잭션이 롤백되었을때 실행
- AFTER_COMPLETION – 트랜잭션이 완료되었을때 실행 (AFTER_COMMIT 및 AFTER_ROLLBACK이 완료되었을때)
- BEFORE_COMMIT – 트랜잭션이 commit 되기 전에 실행
최신 소스는 GitHub 사이트를 참고해 주세요.
https://github.com/codej99/spring-event