- [spring] 스프링 이벤트 (ApplicationEvent와 ApplicationListener/@EventListener와 @TransactionalEventListener/비동기 이벤트)2024년 11월 15일 12시 00분 58초에 업로드 된 글입니다.작성자: @kimyu0218
스프링 이벤트
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver { ... }
스프링의 핵심인 `ApplicationContext`는 `ApplicationEventPublisher`를 상속 받아 이벤트를 게시(publish)하고 구독(listen)할 수 있도록 지원한다. 스프링 이벤트는 RabbitMQ 같은 외부 메시지 브로커와 달리 어플리케이션 내부에서 게시/구독할 목적으로 사용한다.
스프링 이벤트는 다음 장점을 가진다.
- 이벤트를 게시하는 클래스와 이벤트를 구독하는 클래스의 의존 관계를 분리할 수 있다.
- 이벤트를 비동기로 처리할 수 있다.
- 하나의 이벤트를 여러 개의 클래스가 구독할 수 있다.
- 스프링 이벤트로 트랜잭션을 효율적으로 사용할 수 있다.
`MemberService`가 `PushNotificationService`에 의존한다고 가정해보자. 두 서비스는 서로 다른 도메인에도 불구하고 `MemberService` 에서 `PushNotificationService`를 주입받는 구조일 것이다. 즉, 느슨하게 결합되어 있지만 관련 없는 두 기능이 같은 파일에 존재한다. 하지만 스프링 이벤트를 이용하면 이런 의존 관계를 해결할 수 있다.
이벤트를 게시하고 구독하기 위해서는 세 가지 클래스가 필요하다.
- 이벤트 메시지 : 게시 클래스와 구독 클래스 사이에 공유할 데이터를 포함하는 클래스
- 게시 클래스 (= publisher) : 구독 클래스에 전달할 이벤트 메시지를 생성/게시하는 클래스
- 구독 클래스 (= listener) : `ApplicationContext`에서 전달받은 이벤트 메시지를 구독하고 부가 기능을 수행하는 클래스
💡 이벤트는 일종의 DTO로서 게시 클래스 - `ApplicationContext` - 구독 클래스 간에 전달된다.
지금부터 다양한 방법으로 스프링 이벤트를 구현해보자.
ApplicationEvent와 ApplicationListener을 활용한 방법
기존에는 스프링 이벤트를 사용하기 위해 `ApplicationEvent`와 `ApplicationListener`를 사용했다. 이벤트 메시지는 `ApplicationEvent`를 상속받고, 구독 클래스는 `ApplicationListener`를 구현했다.
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { void onApplicationEvent(E event); ... }
위 코드는 `ApplicationListener` 인터페이스다. `ApplicationContext`는 이벤트 메시지 타입을 확인하고 적절한 구독 클래스를 찾아 `onApplicationEvent()`를 실행한다. 따라서 구독 클래스는 `onApplicationEvent()`를 오버라이딩하여 이벤트를 처리해야 한다.
// 이벤트 메시지 public class CreateMemberEvent extends ApplicationEvent { ... } // 게시 클래스 @RequiredArgsConstructor @Service public class MemberService { private final ApplicationEventPublisher applicationEventPublisher; ... void createMember(...) { ... applicationEventPublisher.publishEvent(createMemberEvent); } } // 구독 클래스 @Service public class PushNotificationService extends ApplicationListener<CreateMemberEvent> { ... public void onApplicationEvent(final CreateMemberEvent event) { ... } }
위와 같이 이벤트 메시지, 게시 클래스, 구독 클래스를 구현하면 더 이상 `MemberService`가 `PushNotificationService`를 의존하지 않아도 된다.
@EventListener와 @TransactionalEventListener를 활용한 방법
앞에서는 `ApplicationListener`를 구현했지만 스프링 4.2부터는 `@EventListener` 어노테이션을 이용할 수 있다. 이벤트 메시지 클래스는 더 이상 `ApplicationEvent`를 상속하지 않아도 되며, 구독 클래스도 이벤트를 처리하는 메서드 앞에 `@EventListener`를 붙여주기만 하면 된다.
`@TransactionalEventListener`는 트랜잭션 종료 단계와 연계하여 이벤트를 구독하는 기능을 제공한다. 종료 단계는 다음과 같다.
- `TransactionPhase.BEFORE_COMMIT`
- `TransactionPhase.AFTER_COMMIT` (기본값)
- `TransactionPhase.AFTER_ROLLBACK`
- `TransactionPhase.AFTER_COMPLETION`
@Service public PushNotificationService { @TransactionalEventListener public void handleCreateMemberEvent(final CreateMemberEvent event) { ... } @TransactionalEventListener public void handleLoginMemberEvent(final LoginMemberEvent event) { ... } }
비동기 이벤트
스프링 프레임워크는 비동기 프로그래밍을 위해 `@Async`, `@EnableAsync`를 제공한다. 1) 어플리케이션에 비동기 실행 환경을 구성하고, 2) 비동기로 실행하고 싶은 메서드 앞에 `@Async` 어노테이션을 붙여주면 된다.
먼저 구성 클래스 앞에 `@EnableAsync`를 통해 비동기 실행 환경을 활성화한다. 그리고 스레드 풀을 생성하여 멀티 스레드 환경을 만든다.
@EnableAsync // 비동기 실행 활성화 @Configuration public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { return getThreadPoolTaskExecutor(); } private TaskExecutor getThreadPoolTaskExecutor() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); threadPoolTaskExecutor.setCorePoolSize([CORE_POOL_SIZE]); threadPoolTaskExecutor.setMaxPoolSize([MAX_POOL_SIZE]); threadPoolTaskExecutor.setThreadNamePrefix("[THREAD_NAME_PREFIX]"); return threadPoolTaskExecutor; } }
다음으로 비동기로 실행할 메서드 앞에 `@Async`를 붙여준다. 이때, `@Async`는 AOP로 동작하므로 해당 메서드를 정의한 클래스는 반드시 빈이어야 하며, 메서드는 `public`이어야 한다.
@Service public PushNotificationService { @Async // 해당 메서드를 비동기로 실행한다 @TransactionalEventListener public void handleCreateMemberEvent(final CreateMemberEvent event) { ... } ... }
참고자료
'backend > 프레임워크' 카테고리의 다른 글
[spring] 스프링 부트 시작하기 (스프링/스프링 부트/@SpringBootApplication/ApplicationContext) (1) 2024.12.19 [spring] JPA/Hibernate, Spring Data JPA, 영속성 컨텍스트 (0) 2024.12.16 [spring] REST 구현 (@RestController를 사용하는 이유/ResponseEntity를 사용하는 이유/글로벌 예외 핸들러 정의하기) (0) 2024.11.12 [spring] 스프링 AOP (0) 2024.11.10 [spring] 스프링 컨텍스트 (1) 2024.11.10 다음글이 없습니다.이전글이 없습니다.댓글