호됴! 2022. 5. 22. 12:32
이 글은 https://www.baeldung.com/spring-events 를 번역한 글입니다.

 

1. 개요

이 튜토리얼에서는 스프링에서 어떻게 이벤트를 사용하는지에 대해 다루고자 한다.

이벤트는 프레임워크에서 가장 간과되는 기능 중 하나이면서, 동시에 가장 유용한 기능 중 하나이다. 그리고 이벤트 발행은 다른 스프링의 기능들처럼 ApplicationContext로부터 제공되는 특징 중 하나이다.

 

이벤트 사용을 위해서는 기본적으로 아래의 내용을 따라야 한다.

  • Spring 4.2 이전 버전일 경우, 이벤트 클래스는 ApplicationEvent 를 상속받아야 한다.
    4.2 버전부터는 더 이상 상속받을 필요가 없다.
  • publisher (이벤트 발행자)는 ApplicationEventPublisher 객체를 주입해야 한다.
  • listener (이벤트 리스너)는 ApplicationListener 인터페이스를 구현해야 한다.

 

2. 커스텀 이벤트

스프링은 우리가 custom 이벤트(디폴트로 동기적으로 동작)를 만들고 발행할 수 있도록 해준다.

여기에는 몇 가지 장점이 있는데, 이벤트를 구독하고 있는 리스너이 발행자의 트랜잭션 context 에 참여할 수 있다는 게 그 중 하나이다.

2.1 간단한 어플리케이션 이벤트

이벤트 데이터를 저장할 수 있는 placeholder 만 있는 간단한 이벤트 클래스를 만들어보자.

이 경우, 이벤트 클래스는 문자열 메시지를 가지고 있다.

public class CustomSpringEvent extends ApplicationEvent {
    private String message;

    public CustomSpringEvent(Object source, String message) {
        super(source);
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}

 

2.2 Publisher (발행자)

그럼 이제 그 이벤트의 publisher(발행하는 쪽)을 만들어보자. 퍼블리셔는 이벤트 객체를 구축하고 해당 이벤트를 구독하고 있는(listening) 쪽에게 발행한다.

이벤트를 발행하기 위해서는 퍼블리셔가 ApplicationEventPublisher 객체를 간단히 주입한 후 publishEvent() API를 사용한다.

@Component
public class CustomSpringEventPublisher {
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void publishCustomEvent(final String message) {
        System.out.println("Publishing custom event. ");
        CustomSpringEvent customSpringEvent = new CustomSpringEvent(this, message);
        applicationEventPublisher.publishEvent(customSpringEvent);
    }
}

이 방법 외에도, 퍼블리셔 클래스가 ApplicationEventPublisherAware 인터페이스를 구현하는 방법도 있다. 이 방법 또한 어플리케이션이 시작할 때 이벤트 퍼블리셔를 주입해줄 것이다. 보통은 @Autowire 어노테이션을 사용해서 퍼블리셔를 주입하는 방식이 더 간단하다.

스프링 4.2 버전부터 ApplicationEventPublisher 인터페이스가 이벤트로 어떤 객체든 받을 수 있는 publishEvent(Object event) 메소드를 새롭게 제공한다.

따라서, 스프링 4.2 버전부터는 이벤트 클래스가 더 이상 Application Event 클래스를 상속받을 필요가 없어진다.

2.3 Listener (리스너)

마지막으로, 이제 리스너를 만들어보자.

리스너는 bean이어야 한다는 것과 ApplicationListener 인터페이스를 구현하기만 하면 된다.

@Component
public class CustomSpringEventListener implements ApplicationListener<CustomSpringEvent> {
    @Override
    public void onApplicationEvent(CustomSpringEvent event) {
        System.out.println("Received spring custom event - " + event.getMessage());
    }
}

 

우리의 커스텀 리스너가 매개변수로 아까 정의한 커스텀 이벤트의 제네릭 타입을 매개변수로 받는 것에 주의하자.

이와 같이 제네릭 형태의 매개변수를 통해 onApplicationContext() 메소드를 type-safe 하게 만들어준다. object가 특정 event class 타입인지 체크하고 타입 변환(캐스팅)해주는 과정을 생략할 수 있게도 해준다.

또한 스프링에서의 이벤트는 앞서 언급된 바와 같이 동기적으로 동작하게 되는데, doStuffAndPublishAnEvent() 메소드가 이벤트 처리가 끝날 때까지 모든 리스너를 블락시킨다.

 

3. 비동기 이벤트 만들기

동기적으로 이벤트를 발행하는 게 우리가 원하는 게 아닐 때도 있다. 즉 우리의 이벤트가 비동기적으로 처리될 필요가 되어야 할 때이다

우리는 executor와 함께 ApplicationEventMulticaster 빈을 설정 클래스에 추가하여 비동기로 전환할 수 있다.

우리의 목적에 맞게 SimpleAsyncTaskExecutor 가 잘 동작한다.

@Configuration
public class AsynchronousSpringEventsConfig {
    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster =
          new SimpleApplicationEventMulticaster();
        
        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }
}

이벤트, 퍼블리셔, 리스너 구현은 기존(동기적으로 동작할 때)과 같은데
이제 리스너는 서로 다른 스레드에서 이벤트를 비동기적으로 처리할 것이다.

 

4. 스프링 자체 이벤트

스프링은 다양한 내장 이벤트를 발행해준다. 예를 들면, ApplicationContext 는 다양한 프레임워크 이벤트 ContextRefreshedEvent, ContextStartedEvent, RequestHandledEvent 등 을 발생시킬 것이다.

위 이벤트는 어플리케이션 개발자가 어플리케이션의 생명주기 및 컨텍스트에 접근하여 필요한 부분에 그들이 정의한 로직을 추가할 수 있도록 하는 옵션을 제공해준다.

컨텍스트 새로고침에 대해 리스닝하는 간단한 listener 예제를 보자.

public class ContextRefreshedListener 
  implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent cse) {
        System.out.println("Handling context re-freshed event. ");
    }
}

스프링 내장 이벤트에 대해 좀 더 자세히 알고 싶다면 다음 튜토리얼을 참고하자.

 

5. 어노테이션 기반 이벤트 리스너

스프링 4.2버전부터 이벤트 리스너가 ApplicationListener 인터페이스를 구현하는 빈으로 정의되지 않아도 되는데,

@EventListener 어노테이션으로 관리되는 빈의 모든 public 메소드가 등록되게 된다.

@Component
public class AnnotationDrivenEventListener {
    @EventListener
    public void handleContextStart(ContextStartedEvent cse) {
        System.out.println("Handling context started event.");
    }
}

이전과 마찬가지로, 메소드의 시그니처는 소비하고자 하는 이벤트를 정의한다.

디폴트로 리스너는 동기적으로 불리지만, 우리는 @Async 어노테이션만 추가하여 비동기적으로 만들 수도 있다.

우리는 어플리케이션 단에서 Async 지원을 가능하게 하는 법만 기억하여 적용하면 된다.

2022.05.20 - [번역자료 (구글번역은 못 참지)/Baeldung] - @Async 로 스프링 비동기 처리

 

6. 제네릭 지원

이벤트 타입에서 제너릭 정보를 넣어 이벤트를 dispatch 할 수 있다.

6.1  제네릭 어플리케이션 이벤트

제네릭 이벤트 타입을 만들어보자.

이 예제에서는 이벤트 클래스가 아무 내용과 success 상태 지시자를 갖는다.

public class GenericSpringEvent<T> {
    private T what;
    protected boolean success;

    public GenericSpringEvent(T what, boolean success) {
        this.what = what;
        this.success = success;
    }
    // ... standard getters
}

여기서 GenericSpringEvent 와 CustomSpringEvent의 차이에 주목할 필요가 있다.

이제 어떤 임의의 이벤트든 발행할 수 있어짐에 따라 유연해졌고, 더 이상 ApplicationEvent 를 상속받지 않아도 된다.

 

6.2  리스너

이제 위 이벤트의 리스너를 만들어보자.

아래와 같이 이전처럼 ApplicationListener 인터페이스를 구현해서 리스너를 정의할 수 있다:

@Component
public class GenericSpringEventListener 
  implements ApplicationListener<GenericSpringEvent<String>> {
    @Override
    public void onApplicationEvent(@NonNull GenericSpringEvent<String> event) {
        System.out.println("Received spring generic event - " + event.getWhat());
    }
}

하지만 이렇게되면 다소 성가시게 ApplicationEvent 로부터 내려온 GenericSpringEvent을 상속받아야 한다. 따라서 이 튜토리얼에서는 이전에 언급되었던 어노테이션 기반 리스너를 사용해보자. 

@EventListener에서 boolean 값을 SpEL 표현식을 사용하여 정의해 조건에 따라 이벤트 리스너가 동작할 수도 있다.

이 경우에 이벤트 핸들러는 문자열의 GenericSpringEvent가 성공적일 때만 동작할 것이다.

@Component
public class AnnotationDrivenEventListener {
    @EventListener(condition = "#event.success")
    public void handleSuccessful(GenericSpringEvent<String> event) {
        System.out.println("Handling generic event (conditional).");
    }
}

SpEL은 다른 튜토리얼에서 자세히 다뤄지는데, 강력한 표현 언어다.

 

6.3 퍼블리셔

이벤트 퍼블리셔는 위에서 말한 것과 유사하다. 하지만 타입이 지워지면서, 우리는 필터하고자 하는 제네릭 변수로 제공하여 이벤트를 발행해야 한다.

class GenericStringSpringEvent extends GenericSpringEvent<String>

 와 같이 정의하는 것이다.

 

대안은, @EventListener 가 달린 메소드에서 null이 아닌 값을 반환하면 스프링 프레임워크가 그 결과를 새로운 이벤트로 전달해줄 것이다. 또한 우리는 여러 새로운 이벤트를 발행할 수도 있는데, 이벤트 처리 결과를 컬렉션으로 반환하면 된다.

 

7. 트랜잭션 한정 이벤트

이 부분은 @TransactionalEventListener 어노테이션을 사용할 때에 관한 이야기를 다룬다.

트랜잭션 관리에 대해 더 자세히 알고 싶으면 Transactions With Spring and JPA를 체크해보자.

 

Spring 4.2부터 프레임워크는 새로운 @TransactionalEventListener 어노테이션을 제공한다. 이는 내부에 @EventListener 도 포함하고 있어 트랜잭션 단계를 이벤트 리스너와 바인딩할 수 있도록 해준다.

다음과 같은 트랜잭션 단계에 바인딩이 가능하다.

  • AFTER_COMMIT (디폴트 값) – 트랜잭션이 성공적으로 종료될 경우 이벤트가 발생
  • AFTER_ROLLBACK – 트랜잭션이 롤백될 경우
  • AFTER_COMPLETION – 트랜잭션이 완료될 경우 (AFTER_COMMIT and AFTER_ROLLBACK 을 일컫음)
  • BEFORE_COMMIT – 트랜잭션이 커밋되기 직전에 발생

transactional 이벤트 리스너에 대한 간단한 예제는 다음과 같다.

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleCustom(CustomSpringEvent event) {
    System.out.println("Handling event inside a transaction BEFORE COMMIT.");
}

이 리스너는 이벤트 발행하는 쪽에서 커밋되려 하는 트랜잭션이 있을 때 리스너가 작동할 것이다.

만약 돌고 있는 트랜잭션이 없다면, fallbackExecution 값을 true로 설정하여 오버라이딩하지 않는 한 이벤트는 보내지지 않을 것이다.

 

8. 결론

빠르게 스프링에서 이벤트 처리하는 방법의 기초적인 부분을 알아보았다.

간단한 커스텀 이벤트를 만들어보고 발행한 후 리스너에서 처리해보았다.

또한 간단하게 설정에서 이벤트를 비동기적으로 처리되도록 하는 방법도 간단히 알아보았다.

그리고 Spring 4.2에서의 개선사항을 공부해보았다.

(어노테이션 기반의 리스너, 더 나은 제네릭 지원, 그리고 트랜잭션 단계에 이벤트 바인딩 하는 등)

 

항상 그래왔듯 이 글에서 보인 코드는 github에서 확인할 수 있다.