저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

다음과 같은 간단한 이벤트 관련 코드를 만들 일이 생겼다.

  1. 도메인 객체가 트랜잭션 범위에서 이벤트를 발생하면 핸들러로 처리
  2. 트랜잭션이 커밋된 이후에 이벤트 핸들러에 이벤트 전달해야 함
  3. 이벤트가 유실되어 처리하지 못해도 됨(실패시 후처리)
  4. 이벤트 핸들러는 비동기로 실행

스프링 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


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. coding8282 2017.03.19 09:43 신고  댓글주소  수정/삭제  댓글쓰기

    저는 Domain Event Handler를 따로따로 만들어서 사용했었는데, 이 방법을 적용하니 정말 좋네요. 비동기 처리도 간단하구요~... 응용 범위가 굉장히 넓을 것 같습니다~~~ 감사합니다

  2. tigmi 2018.01.25 22:58 신고  댓글주소  수정/삭제  댓글쓰기

    마지막에 말씀해주신 것처럼 @Async와 @TransactionalEventListener를 동시에 설정했을 경우
    제가 로컬에서 시도해봤을 때는
    1.transactionA에서 handle이 호출 될 경우
    2. @Async가 먼저 실행 되어 새로운 thread에서 새로운 transactionB가 시작되고
    3. transactionB가 commit되었을 때 실제 handle 함수가 실행되는 걸로 알고 있는데,
    의도하신 대로 transactionA가 commit된 이후에 async하게 handle 함수가 실행되나요?

    • 최범균 madvirus 2018.02.05 12:43 신고  댓글주소  수정/삭제

      https://github.com/madvirus/event-sample 에 간단한 예제를 올렸습니다.

      github에서 받으신 뒤에 EventSampleApplication을 실행해보시면 됩니다.

    • tigmi 2018.02.06 18:36 신고  댓글주소  수정/삭제

      확인해보니 제가 예전에 @TransactionalEventListener를 잘못된 방법으로 사용하고 있었네요 .공유 감사합니다!!