티스토리 뷰

Spring

[Spring] 스프링에서 이벤트의 발행과 구독 방법과 주의사항, 이벤트 사용의 장/단점과 사용 예시

망나니개발자 2023. 5. 9. 10:00
반응형

이벤트(Event)는 매우 유용하지만 상당히 간과되는 기능 중 하나입니다. 작년에 아마존 CTO는 이벤트 드리븐 아키텍처로 가야 한다고 기조 연설을 하기도 했는데, 이번에는 스프링 프레임워크에서 이벤트(Event)를 사용하는 방법에 대해 알아보겠습니다.

 

 

 

 

1. 스프링에서 이벤트 사용법 및 주의사항


[ 스프링에서 이벤트의 발행과 구독 ]

스프링은 이벤트를 발행하고 구독하는 기능을 제공하고 있는데, 해당 기능은 아래와 같은 기본적인 가이드를 따라야 한다.

  • 스프링 부트 1.3(엄밀히는 스프링 4.2) 이전 버전이라면 이벤트 클래스가 ApplicationEvent를 상속해야 함
  • 이벤트 발행을 위해서는 ApplicationEventPublisher를 주입받아 사용해야 함
  • 이벤트 구독을 위해서는 ApplicationListener 인터페이스를 구현하거나 @EventListener를 사용해야 함

 

 

스프링 4.2 이전에는 반드시 이벤트 클래스가 ApplicationEvent 클래스를 상속받아야 했다. 하지만 4.2부터는 해당 클래스를 상속받지 않고도 이벤트를 발행 및 구독할 수 있도록 하였다.

스프링에서 이벤트 발행은 ApplicationEventPublisher 인터페이스가 담당하는데, 이를 구현하는 것은 결국 애플리케이션 컨텍스트(ApplicationContext)이다. 애플리케이션 컨텍스트는 많은 책임(빈 탐색과 등록, 리소스 처리 등)을 제공하고 있는데, 인터페이스 분리 원칙(ISP, Interface Segregation Principle)에 맞게 이벤트 발행 책임만 처리하는 것이 바로 ApplicationEventPublisher 인터페이스이다. 이벤트 발행은 해당 인터페이스의 publishEvent 메소드를 사용하면 된다.

@Service
@RequiredArgsConstructor
public class MangKyuEventPublisher {

    private final ApplicationEventPublisher publisher;

    public void publish() throws InterruptedException {
        publisher.publishEvent(new MangKyuEvent("MangKyuEvent"));
    }

}

 

 

이벤트 구독은 ApplicationListener 인터페이스를 구현하거나 @EventListener를 사용하면 된다. 스프링 4.2 이전에는 이벤트를 구독하려면 반드시 ApplicationListener 인터페이스를 구현해주어야 했다. 그래서 ApplicationListener 인터페이스의 타입을 보면 ApplicationEvent를 확장한 제네릭 타입임을 볼 수 있다. 이벤트가 발행되면 애플리케이션 컨텍스트는 해당 이벤트를 구독하는 빈들을 찾아서 notify 해주는데, 이러한 부분은 내부적으로는 옵저버 패턴을 사용해 구현되어 있다.

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

	void onApplicationEvent(E event);

	static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
		return event -> consumer.accept(event.getPayload());
	}

}

 

 

하지만 ApplicationListener 인터페이스를 직접 구현하는 방식은 상당히 번거롭다. 그래서 스프링 4.2부터는 @EventListener 어노테이션을 제공하여 편리하게 이벤트를 구독할 수 있도록 도와주고 있다. 참고로 스프링 부트 역시 애플리케이션 이벤트를 만들어서 사용하고 있는데, 관련된 자세한 내용은 해당 포스팅에서 참고하도록 하자.

@Component
public class MangKyuEventListener {

    @EventListener
    public void listen(MangKyuEvent event) {

    }
}

 

 

 

 

[ 스프링에서 이벤트의 발행과 구독 ]

  • 멀티캐스팅 방식으로 동작
  • 동기 방식으로 동작함
  • 트랜잭션과의 결합이 필요하다면 다른 리스너를 사용해야 함

 

 

스프링의 이벤트 리스너는 기본적으로 멀티 캐스팅 관계이다. 멀티 캐스팅이란 다수의 수신자가 존재할 수 있는 통신 형태이므로, 따라서 동일한 타입의 여러 리스너가 등록되었다면 모든 리스너가 이벤트를 받게 되므로 주의가 필요하다.

또한 스프링 이벤트는 기본적으로 동기 방식으로 동작한다. 동기 방식으로 동작하는 것이 중요한 이유는 트랜잭션이 하나의 범위로 묶일 수 있기 때문이다. 만약 이벤트를 발행하는 곳에서 트랜잭션이 시작된 상태라면 이벤트를 구독하는 곳에서도 동일한 트랜잭션을 공유하게 된다. 비동기 방식으로 이벤트를 동작시키려면 별도의 설정이 필요한데, 해당 부분은 아래에서 살펴본다.

이벤트 처리가 하나의 트랜잭션의 안에서 실행된다면 트랜잭션의 커밋 전/후, 완료 또는 롤백에 따라 동작을 고려해야할 수 있다. 스프링 4.2부터는 @EventListener를 확장한 @TransacitionEventListener를 제공하고 있으며, 다음과 같은 옵션을 통해 트랜잭션 단계에 관여할 수 있도록 도와준다.

  • TransactionPhase.BEFORE_COMMIT
  • TransactionPhase.AFTER_COMPLETION
  • TransactionPhase.AFTER_COMMIT
  • TransactionPhase.AFTER_ROLLBACK

 

 

 

[ 비동기로 이벤트 처리하기 ]

참고로 이벤트를 비동기 방식으로 동작시키는 것은 이벤트 발행를 발행하기 전/후가 더 이상 하나의 트랜잭션으로 묶일 수 없다는 것을 의미하기도 한다. 따라서 정밀하게 트랜잭션을 사용하는 경우라면 이러한 부분을 반드시 염두해두어야 한다.

이벤트를 비동기로 처리하기 위해서는 다음의 2가지 방법이 있다.

  • @Async 메소드로 비동기 구현
  • ApplicationEventMulticaster로 비동기 구현

 

 

기본적으로 이벤트를 구독하는 부분에 @Async 메소드를 사용하면 된다. 그러면 이벤트를 구독하는 부분을 작성해줄 수 있다. 만약 @Async를 붙여줘도 비동기 실행이 되지 않는다면 @EnableAsync로 비동기 설정이 활성화 되어 있는지 확인해보면 된다.

@Component
public class MangKyuEventListener {

    @Async
    @MangKyuListener
    public void listen(MangKyuEvent event) {

    }
}

 

 

일부 메세지만 비동기 처리할 경우에는 위와 같이 처리하면 된다. 하지만 모든 메세지들을 기본적으로 비동기 처리할 것이라면 위와 같은 방식은 번거롭다. 다수의 ApplicationListener 객체를 관리하고 객체에 이벤트를 전달하기 위한 인터페이스는 ApplicationEventMulticaster인데, 해당 부분을 수정하면 된다.

스프링은 applicationEventMulticaster라는 이름의 빈을 찾고, 없으면 기본적으로 만들어둔 SimpleApplicationEventMulticaster를 생성해 빈으로 등록한다. 그래서 비동기로 동작하는 ApplicationEventMulticaster 객체를 만들면 되는데, 해당 부분은 다음과 같이 작성하면 된다. 참고로 여기서 bean의 이름을 명시적으로 지정해 주는 것이 좋다.

import static org.springframework.context.support.AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME;

@Configuration
public class ApplicationEventConfig {


    /* 해당 부분은 스프링 프레임워크 스펙에 따라 정해진 이름대로 빈 등록을 해주어야 함*/
    @Bean(name = APPLICATION_EVENT_MULTICASTER_BEAN_NAME)
    public ApplicationEventMulticaster applicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
        eventMulticaster.setTaskExecutor(asyncExecutor());
        return eventMulticaster;
    }
	
    private Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(10000);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(10);
        executor.initialize();
        return executor;
    }
}

 

 

왜냐하면 애플리케이션 컨텍스트에서 ApplicationEventMulticaster를 찾을 때 해당 타입으로 찾는 것이 아니라 applicationEventMulticaster라는 빈의 이름으로 찾기 때문이다. 그래서 빈의 이름을 다르게 지정해주면 비동기로 동작하지 않는다. 실제로 AbstractApplicationContext에 다음과 같이 구현되어 있다.

protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
        this.applicationEventMulticaster =
            beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
            if (logger.isTraceEnabled()) {
                logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
        }
    } else {
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
        beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
        if (logger.isTraceEnabled()) {
            logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +
                "[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");
        }
    }
}

 

 

이러한 부분과 관련된 이슈도 있는데, 스프링의 아버지인 위르겐 휠러(Juergen Hoeller)는 다음과 같은 코멘트를 남겼다.

 

 

스프링의 디스패처 서블릿에는 applicationEventMulticaster를 포함해 loadTimeWeaver, viewNameTranslator 등 빈의 이름을 사용하는 부분이 남아있으며, 위임의 관점에서 빈이 하나만 있어야 하는 곳에서는 빈의 이름을 사용하는 것이 규칙이라고 한다. 그리고 해당 규칙을 재정의하지 않는 한 이를 수정하면 혼란을 줄 수 있으므로 수정하지 않겠다고 하였다. 이러한 부분은 스프링6까지도 유효하기 때문에 빈의 이름을 applicationEventMulticaster로 등록해주는 부분을 절대 빼먹어서는 안된다. 그리고 이러한 부분은 다른 개발자 분들도 알 수 있도록 설정 파일에 주석으로 남겨두는 것이 좋다.

 

 

 

 

 

2. 이벤트 사용의 장점과 단점 및 사용 예시


[ 스프링에서 이벤트의 발행과 구독 ]

  • 장점
    • 의존성을 분리하여 두 클래스를 느슨하게 결합시킬 수 있음
    • 클래스가 독립적이므로 재사용성을 높일 수 있음
    • 추후에 별도의 서비스로 분리하기 용이함
    • 메세지 구독 모듈을 추가 또는 삭제할 때, 다른 모듈에 영향을 주지 않은 채로 수정할 수 있음
    • 단위 테스트가 용이해짐
  • 단점
    • 전반적인 작업량이 많아질 수 있음(이벤트 클래스, 커스텀 어노테이션 등)
    • 코드 흐름을 따라가기 어려움
    • 메세지 구독 순서를 고려해야 하는 경우 복잡해짐
    • 전체적인 이벤트의 구독 및 발행 과정을 테스트하기 어려움
    • 특정 프레임워크 API에 의존하게 됨

 

 

이벤트를 사용했을 때의 장점은 결국 두 도메인 간의 의존성을 완전히 분리할 수 있다는 것이다. A 클래스는 B 클래스의 존재를 몰라도 되기 때문에 두 클래스는 느슨하게 결합되며, 재사용성도 높아지고, 추후에 별도의 서비스로 분리하기도 쉬워진다. 또한 해당 메세지를 처리해야 하는 모듈이 추가되거나 삭제되어도, 기존의 모듈은 영향을 받지 않게 된다.

하지만 이벤트 클래스를 생성하고, 구독을 위한 커스텀 어노테이션을 만드는 등의 부가적인 작업들이 필요할 수 있다. 또한 이벤트를 사용하면 기본적으로 전반적인 코드 흐름을 따라가기가 어려워지며, 특히 여러 개의 리스너가 구독하는 상황에서 순서까지 고려해야 한다면 유지보수하기 더욱 어려워진다. 그리고 단일 모듈은 테스트 하기 쉬워질 수도 있지만 전체적인 이벤트의 구독과 발행을 테스트하기도 어려울 뿐만 아니라 특정 프레임워크의 API에 결합하게 된다는 단점도 있다.

 

 

 

 

[ 언제 이벤트를 사용해야 하는가? ]

이벤트를 사용해야 하는 경우는 특정한 도메인의 상태 변경을 외부로 알려주어야 하는 경우이다.

예를 들어 주문이 완료되었으면 Order 도메인에서 다른 도메인으로 변경에 따른 처리를 해야 하는 상황에 필요하다.

이러한 경우는 일반적으로 트랜잭션이 커밋되는 시점과 일치하는 경우가 많은데, spring-data 프로젝트는 이를 위한 Domain Event를 제공하고 있다. 자세한 내용은 여기를 참고하도록 하자.

 

 

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/12   »
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
글 보관함