티스토리 뷰

Spring

스프링 이벤트에 대해 알아보자

perseverance 2024. 10. 2. 19:18

스프링에서 이벤트는 강하게 결합된 의존성을 느슨하게 만들어주는 역할을 할 수 있다. 예를들어 주문 시스템에서 주문이 완료되면 주문 알림을 준다던가 주문 수량을 증가시키는 일을 하게될때 주문 시스템은 후속작업을 위한 여러 서비스들을 의존하게 된다. 이때 스프링 이벤트를 사용하면 이러한 의존성을 느슨하게 만들어 줄 수 있다.

 

 

1. 스프링에서 이벤트 사용법


이벤트를 발행하기 위해서는 ApplicationEventPublisher를 주입받으면 된다. ApplicationEventPublisher을 주입받기 위해서는 첫번째로 ApplicationEventPublisherAware 인터페이스를 구현한 class를 빈으로 등록해주면 된다.

@Service
public class EventService implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher publisher;

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
}

 

Spring 컨테이너는 EventService가 ApplicationEventPublisherAware를 구현한 것을 감지하고 자동으로 setApplicationEventPublisher를 호출해준다고 한다. 이때 매개변수로 주입해주는 구현체는 AbstractApplicationContext이다.

위와 같이 하거나 아니면 @Autowired를 통해 주입받으면 이벤트를 발행할 준비가 완료된다. 

 

이벤트 발행을 위해서는 ApplicationEventPublisher 인터페이스의 publishEvent 메서드를 사용하면 된다.

@Service
@RequiredArgsConstructor
public class EventServiceV2 {

    private final ApplicationEventPublisher publisher;

    public void sendEmail(String address, String content) {
        publisher.publishEvent(new BlockedListEvent(this, address, content));
        // send email...
    }
}

 

 

이벤트 구독은 ApplicationListenr 인터페이스를 상속하거나 @EventListener을 사용하면 된다. 스프링 4.2 이전에는 @EventListner를 사용할 수 없었지만 스프링 4.2부터 @EventListner을 사용하여 관리되는 빈의 모든 메서드에 리스너를 등록할 수 있게 됐다. 

@Component
public class BlockedListNotifier {
    /**
     * spring 4.2부터 @EventListener 을 사용하여 관리되는 빈의 모든 메서드에 이벤트 리스너를 등록할 수 있음
     */
    @EventListener
    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }

 

이벤트를 구독하는 방법은 다양한데 여러 이벤트를 구독할 수도 있고 SpEL 표현식을 사용해서 이벤트를 구독할수도 있다.

/**
 * 여러 이벤트를 수신할 수 있음
 */
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

/**
 * SpEL 표현식을 사용해서 이벤트를 수신할 수 있음
 */
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

 

또 이벤트를 처리한 결과로 이벤트를 발행하고 싶은 경우가 생길 수 있는데 이때는 메서드 시그니처에 발행하기 원하는 이벤트를 두면된다.

/**
 * 이벤트를 처리한 결과로 이벤트를 발행하고 싶다면 다음과 같이 메서드 시그니처에 발행하기 원하는 이벤트를 두면됨
 */
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
    return new ListUpdateEvent();
}

 

기본적으로 @EventListner는 이벤트를 동기식으로 수신한다. 이게 무슨 말이냐면 Class1이 이벤트를 발행해 Class2가 이벤트를 수신했다면 Class2가 이벤트를 완전히 처리하기 전까진 Class1은 기다려야 한다는 소리이다. 또한 Class2에서 예외가 발생한다면 해당 예외는 Class1까지 전파된다. 동기식의 장점은 이벤트를 발행한 곳과 이벤트를 수신한 곳 모두 같은 쓰레드를 사용하므로 ThreadLocal 및 로깅 컨텍스트(MDC)을 공유한다. 정말 그런지 한번 테스트 해보았다.

 

간단하게 MDC에 hello라는 key로 world값을 저장한다. 그리고 이벤트를 발행한다.

@Service
@Slf4j
@RequiredArgsConstructor
public class EventServiceV2 {

    private final ApplicationEventPublisher publisher;

    public void sendEmail(String address, String content) {
        MDC.put("hello","world");
        publisher.publishEvent(new BlockedListEvent(this, address, content));
        // send email...
    }
}

 

이벤트를 수신하는 쪽은 MDC에서 hello의 키 정보를 가져와 출력한다.

@EventListener
public void onApplicationEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
    String value = MDC.get("hello");
    System.out.println("value = " + value);
}

 

테스트를 돌려본 결과 world값을 가져온것을 확인할 수 있다.

 

이벤트가 동기 방식으로 동작하는 것이 중요한 이유 중 하나는 트랜잭션을 하나의 범위로 묶을 수 있기 때문이다. 트랜잭션 매니저는 내부적으로 ThreadLocal을 사용하여 트랜잭션 상태를 저장하고 관리하기 때문에, 같은 쓰레드에서 실행되는 동기 이벤트 리스너는 동일한 트랜잭션 범위 내에서 처리될 수 있다. 여기서 주의사항은 트랜잭션을 공유하기 때문에 이벤트를 발행하는 쪽에서 예외가 생기지 않아도 이벤트를 수신하는 쪽에서 예외가 발생한다면 예외가 전파되어 트랜잭션 롤백이 될 수 있다. 주의하자.. 

 

그래서 스프링에서는 트랜잭션의 커밋전, 커밋 후, 완료, 롤백에 따라 이벤트가 수신할 수 있게 @TransactionEventListner를 제공하고 있다.

 

2. 스프링에서 이벤트 비동기 사용법


때로는 이벤트 수신을 비동기로 처리하고 싶을 수 있다. 이벤트를 비동기로 처리하기 위해서는 간단하게 @Async 어노테이션을 사용하면 된다.

@EventListener
@Async
public void processBlockedListEventAsync(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}

 

이벤트를 비동기적으로 사용하면 주의사항이 있는데 먼저 이벤트 리스너가 예외를 던지면 이벤트를 발행한 class까지 전파되지 않는다. 비동기에서 던져진 예외를 관리하기 위해서는 따로 AsyncUncaughtExceptionHandler를 구현해야한다. 또한 비동기 이벤트 리스너는 메서드 값을 반환하여 후속 이벤트를 발행하던 방식을 사용할 수 없게 된다. 이벤트를 발행하고 싶다면 따로 ApplicationEventPublisher를 주입해서 사용하면된다. 마지막으로 이벤트 리스너는 ThreadLocal과 로깅 컨텍스트(MDC)가 전파되지 않는다. 정말 그런지 테스트해보았다.

 

이벤트를 발행하는 곳에서는 아까와 같이 world값을 저장해주었다. 

public void sendEmail(String address, String content) {
    MDC.put("hello","world");
    publisher.publishEvent(new BlockedListEvent(this, address, content));
    // send email...
}

 

이벤트를 수신하는 곳에서는 value값을 꺼내서 출력한다.

@EventListener
@Async
public void processBlockedListEventAsync(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
    String value = MDC.get("hello");
    System.out.println("async value = " + value);
}

 

결과를 보면 null값이 출력된걸 볼 수 있다.

 

ThreadLocal값을 공유하지 않으니 비동기 이벤트 수신 메서드에서는 이벤트를 발행하는 곳의 트랜잭션과 함께 묶일 수 없다는것을 의미하기도 한다. 따라서 이벤트를 수신하는 쪽의 메서드에서 JPA를 이용해 엔티티를 저장하거나 업데이트한다면 트랜잭션이 없어 반영이 안될 수 있다는 것을 명심하자. 이것도 정말 그런지 한번 테스트해보았다.

 

이벤트를 발행하는 쪽에서 TranscationManager를 사용해 트랜잭션 이름과 활성화 여부를 출력해주었다.

@Transactional
public void sendEmail(String address, String content) {
    log.info("TransactionName: {}", TransactionSynchronizationManager.getCurrentTransactionName());
    log.info("TransactionActive: {}", TransactionSynchronizationManager.isActualTransactionActive());
    publisher.publishEvent(new BlockedListEvent(this, address, content));
    // send email...
}

 

이벤트를 수신하는 곳에서는 비동기로 수신하고 여기서도 TranscationManager를 사용해 트랜잭션 이름과 활성화 여부를 출력해주었다.

@EventListener
@Async
public void processBlockedListEventAsync(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
    log.info("TransactionName: {}", TransactionSynchronizationManager.getCurrentTransactionName());
    log.info("TransactionActive: {}", TransactionSynchronizationManager.isActualTransactionActive());
}

 

테스트를 돌려본 결과 역시 예상했던대로 이벤트를 발행하는 class에는 트랜잭션 이름과 활성화 여부가 출력되지만 비동기로 이벤트를 수신하는 곳에서는 트랜잭션이 없어 null값을 반환하게 된다.

 

비동기도 사용하고 트랜잭션도 사용하고 싶다면 다음과 같이 새 트랜잭션을 만들어주면 된다.

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Async
public void processBlockedListEventAsync(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
    String value = MDC.get("hello");
    System.out.println("async value = " + value);
}

 

 

참고자료

 

Additional Capabilities of the ApplicationContext :: Spring Framework

As discussed in the chapter introduction, the org.springframework.beans.factory package provides basic functionality for managing and manipulating beans, including in a programmatic way. The org.springframework.context package adds the ApplicationContext i

docs.spring.io

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함