BeanPostProcessor로 메소드에 ApplicationEvent AOP등록하기
2023-02-28 15:21
Spring AOP 기술을 사용하여, 비즈니스 모델의 상태가 변화할 때 SpringApplicationEvent를 발행하는 방법을 설명합니다.
개요
Spring Core Conception 중 하나인 AOP(Aspect-Oriented Programming)란 관점 지향 프로그래밍이라고 하는 기법이며, 기존 OOP가 클래스 기준 모듈화라면 관점 기준의 모듈화라고 볼 수 있다. 본 포스팅은 Spring의 AOP 기술만을 이용하여, 메소드 호출시 등록한 SpringApplicationEvent를 발급하는 AOP가 동작하게 하는 방법을 설명한다. 관심사는 비즈니스 모델의 상태변화 이벤트를 발행하는 것이었으며, 이 관심사를 해결하기 위해 취했던 방법을 소개한다.
관심사를 붙일 인터페이스
데이터가 DB에 저장되고 난 다음 이벤트가 발행되길 원한다.
//사용자가 정의한 메소드 인터페이스
public interface DomainRepository<T> {
@Transactional // TransactionalEventListner 에서 캐치할 수 있게 한다.
void save(T t); // save 메소드가 호출된 다음, AOP 가 동작하길 원한다.
}
AOP 설정
1) Pointcut, Advice 설정
Pointcut은 전체 관심사(Aspect)를 종단했을 때 걸리는 JoinPoint들의 실제 타겟이며, 부분집합이다. 어떤 메소드를 찾을 것인지를 지정하는 역할을 한다. Advice 는 Pointcut에 실제 행하는 동작을 의미한다.
@Aspect
@Component
@RequiredArgsConstructor
public class DomainRepositoryAspect {
private final ApplicationEventPublisher applicationEventPublisher;
@Pointcut("within(com.example.repository.DomainRepository+)")
public void domainRepositories() {}
@Pointcut("execution(* *..save*(..))")
public void saveMethods() {}
@AfterReturning(value = "domainRepositories() && saveMethods()")
public void invokeVoid(JoinPoint joinPoint) throws Throwable {
//Advice
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof Demo) {
Demo demo = (Demo) arg;
if (!CollectionUtils.isEmpty(demo.domainEvents())) {
demo.domainEvents().forEach(applicationEventPublisher::publishEvent); // 1번) 도메인 이벤트 리스트가 있으면 퍼블리싱한다.
demo.clearDomainEvents(); // 2번) 리스트를 클리어한다.
}
}
}
}
}
@AfterReturning(value = "domainRepositories() && saveMethods()")
- 표현식을 통해 com.example.repository.DomainRepository를 상속하면서 메소드명 save를 포함하는 것을 PointCut으로 지정한다.
- @AfterReturning 은 Transaction AOP가 아직 커밋되지 않고 save 메소드가 반환된 직후 실행되는 시점이다.
2) BeanPostProcessor로 AOP 등록
AOP 등록 방식은 다양한 방법이 있지만, 본 포스팅은 BeanPostProcessor 방법으로 등록하였다. BeanPostProcessor란 Spring에서 Bean의 생성 및 초기화 전과 후 시점에 실행되는 구현체들이다.
@Component
public class DomainRepositoryPostProcessor implements BeanPostProcessor {
private final DomainRepositoryAspect domainRepositoryAspect;
public DomainRepositoryPostProcessor(DomainRepositoryAspect domainRepositoryAspect) {
this.domainRepositoryAspect = domainRepositoryAspect;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof DomainRepository) {
new ProxyFactory(bean).addAdvice(domainRepositoryAspect); // 내 관심사를 DomainRepository 구현체에 붙인다.
}
return bean;
}
}
- Bean 초기화 후에 Repository Bean이 DomainRepository인터페이스를 구현하는 경우, DomainRepositoryPostProcessor클래스가 앞서 선언한 AOP를 등록하여 추가한다.
- Spring data의 EventPublishingRepositoryProxyPostProcessor클래스와 유사하게 ProxyBean을 등록한다.
3) 이벤트 리스닝
관심사는 AOP 코드에 의해 발행되었고, 이제 발행된 이벤트를 소비만 하면 된다.
@Component
@Async
class DomainEventToChannelHandler {
// Transaction이 정상적으로 Commit 되었을 때만 리스닝 하고 싶을때
@TransactionalEventListener
public void eventToChannel(DomainEvent event) {
log.info("[event-to-channel] {}", event);
if (event instanceof DemoEvent) {
// 리스닝한 이벤트로 하고싶은 일을 여기에 작성한다.
log.info("Got it during Transactional")
doSendAppPush(event);
}
}
// Transaction과 관계없이 리스닝하고 싶을때
@EventListener
public void eventToChannel(DomainEvent event) {
log.info("[event-to-channel] {}", event);
if (event instanceof DemoEvent) {
// 리스닝한 이벤트로 하고싶은 일을 여기에 작성한다.
log.info("Got it when Event was published")
}
}
}
- 정상적으로 Transaction이 Commit된 후의 이벤트를 리스닝하는 리스너 @TransactionalEventListener
- 이벤트가 퍼블리싱 되면 리스닝하는 리스너 @EventListener
리스너를 언제 쓰지?
아래는 save가 일어난 직후 찍힌 로그이며, 관심사는 상태 변경을 캐치해서 AppPush 메세지를 호출하고 싶었다.
잘 동작하는 모습!
결론
이번 포스팅에서는 Spring AOP와 ApplicationEvent를 활용하여 비즈니스 모델의 상태 변화 이벤트를 발행하는 방법에 대해 다뤄보았다. Spring AOP를 이용하여 메소드 호출 시 발생하는 이벤트를 캐치하고, ApplicationEventPublisher를 이용하여 발행하는 방법을 소개했으며 마지막에는 발행된 이벤트를 리스닝하는 방법에 대해 알아보았다. 비즈니스 로직과 관련된 이벤트를 통해 시스템 구성을 유연하게 조절하는 것은 유용하며, 이번 글이 Spring 개발자들에게 도움되길 바란다.