다음과 같은 간단한 이벤트 관련 코드를 만들 일이 생겼다.
- 도메인 객체가 트랜잭션 범위에서 이벤트를 발생하면 핸들러로 처리
- 트랜잭션이 커밋된 이후에 이벤트 핸들러에 이벤트 전달해야 함
- 이벤트가 유실되어 처리하지 못해도 됨(실패시 후처리)
- 이벤트 핸들러는 비동기로 실행
스프링 4.2나 그 이후 버전을 사용한다면 아주 간단하게 위 조건을 충족하는 코드를 만들 수 있다. 다음 조합을 사용하면 된다.
- ApplicationEventPublisher.publishEvent(Object event) 사용
- @TransactionEventListener
- @Async로 비동기 처리
1. Events 클래스
다음은 도메인 객체에서 이벤트를 발생시킬 때 사용할 Events 클래스이다.
import org.springframework.context.ApplicationEventPublisher;
public class Events {
private static ThreadLocal<ApplicationEventPublisher> publisherLocal =
new ThreadLocal<>();
public static void raise(DomainEvent event) {
if (event == null) return;
if (publisherLocal.get() != null) {
publisherLocal.get().publishEvent(event);
}
}
static void setPublisher(ApplicationEventPublisher publisher) {
publisherLocal.set(publisher);
}
static void reset() {
publisherLocal.remove();
}
}
Events 클래스의 raise() 메서드는 ApplicationEventPublisher를 이용해서 이벤트를 퍼블리싱한다. 참고로 raise() 메서드의 event 파라미터는 Event 타입인데 이 타입은 원하는 타입으로 알맞게 만들면 된다.
도메인 객체에서 Events.raise()로 발생한 이벤트를 ApplicationEventPublisher로 퍼블리싱하려면 도메인 객체를 실행하기 전에 Events.setPublisher()로 ApplicationEventPublisher를 설정해야 한다. 이를 위해 다음의 Aspect를 구현했다..
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class EventPublisherAspect implements ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
private ThreadLocal<Boolean> appliedLocal = new ThreadLocal<>();
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object handleEvent(ProceedingJoinPoint joinPoint) throws Throwable {
Boolean appliedValue = appliedLocal.get();
boolean nested = false;
if (appliedValue != null && appliedValue) {
nested = true;
} else {
nested = false;
appliedLocal.set(Boolean.TRUE);
}
if (!nested) Events.setPublisher(publisher);
try {
return joinPoint.proceed();
} finally {
if (!nested) {
Events.reset();
appliedLocal.remove();
}
}
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.publisher = eventPublisher;
}
}
트랜잭션 범위에서 실행되는 경우에만 이벤트를 처리하기 위해 @Transactional을 적용한 경우에만 적용하도록 설정했다. 대상 메서드를 실행하기 전에 Events.setPublisher()를 이용해서 스프링의 ApplicationEventPublisher를 설정하고, 대상 메서드를 실행한 후에 Events.reset()으로 초기화하도록 했다.
이제 트랜잭션 범위에서 실행되는 도메인 객체는 다음과 같은 코드를 이용해서 이벤트를 발생시키면 된다.
public class Order {
public void cancel() {
...
Events.raise(new OrderCanceledEvent(this.id));
}
}
2. @TransactionEventListener로 이벤트 핸들러 구현하기
스프링 4.2 이전까지는 트랜잭션과 동기화해서 뭘 실행하려면 TransactionSynchronizationManager를 사용해야 했는데, 스프링 4.2에 들어간 @TransactionEventListener를 사용하면 손쉽게 트랜잭션 커밋 이후에 이벤트 핸들러를 실행할 수 있다.
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class EventHandler {
@TransactionalEventListener
public void handle(OrderCanceledEvent event) {
// ... 이벤트 처리
}
@TransactionalEventListener의 phase 속성을 사용하면 트랜잭션 커밋 이후뿐만 아니라 커밋 전, 롤백 이후, 커밋이나 롤백 이후에 이벤트를 처리하도록 설정할 수 있다.
트랜잭션 여부에 상관없이 이벤트 발생 시점에 이벤트를 처리하고 싶다면 @EventListener를 사용하면 된다.
3. @EnableAsync와 @Async로 비동기로 핸들러 실행하기
이벤트 핸들러를 비동기로 처리하고 싶다면 @EnableAsync와 @Async를 사용하면 된다. 스프링 설정 클래스에 @EnableAsync를 추가했다면, @TransactionalEventListener와 @Async를 함께 사용해서 이벤트를 트랜잭션 커밋 이후에 비동기로 처리할 수 있다.
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class EventHandler {
@Async
@TransactionalEventListener
public void handle(OrderCanceledEvent event) {
// ... 이벤트 처리
}
간단한 샘플
https://github.com/madvirus/event-sample 에서 간단한 샘플 코드를 다운로드 받을 수 있다. 트랜잭션 완료 후에 비동기로 실행되는 예를 보여주기 위해 SampleService에 다음과 같이 슬립 시간을 주었다.
@Service
public class SampleService {
private Logger logger = LoggerFactory.getLogger(getClass());
private JdbcTemplate jdbcTemplate;
@Transactional
public void doSome() {
logger.info("raise event");
Events.raise(new SampleEvent());
try {
Thread.sleep(2000L); // 이벤트 발생후 2초 슬립
} catch (InterruptedException e) {
}
Integer result = jdbcTemplate.query("select 1", new ResultSetExtractor<Integer>() {
@Override
public Integer extractData(ResultSet resultSet) throws SQLException, DataAccessException {
resultSet.next();
return resultSet.getInt(1);
}
});
logger.info("doSome: query result = {}", result);
try {
Thread.sleep(2000L); 쿼리 실행후 2초 슬립
} catch (InterruptedException e) {
}
}
예제 프로젝트의 이벤트 핸들러는 다음과 같다.
@Component
public class EventHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Async
@TransactionalEventListener
public void handle(SampleEvent event) {
logger.info("handle event");
}
}
예제를 실행하면 다음과 같은 결과가 출력된다. 결과 로그를 보면 이벤트를 발생한 뒤에 EventHandler가 바로 실행되지 않은 것을 알 수 있다. 또한, 이벤트를 발생시킨 쓰레드 이름이 "main"이고 EventHandler를 실행한 쓰레드 이름이 "cTaskExecutor-1"인데 이를 통해 이벤트를 발생시킨 쓰레드가 아닌 다른 쓰레드에서 이벤트를 비동기로 처리했음을 알 수 있다.
2018-02-05 12:47:21.794 INFO 20636 --- [ main] eventsample.app.SampleService : raise event
2018-02-05 12:47:23.830 INFO 20636 --- [ main] eventsample.app.SampleService : doSome: query result = 1
2018-02-05 12:47:25.869 INFO 20636 --- [cTaskExecutor-1] eventsample.app.EventHandler : handle event