Programming/Spring Boot

spring 이벤트 사용하기(event publisher, event listener)

Jan92 2022. 12. 23. 23:26

최근 프로젝트에서 'Spring Event'를 사용하게 되면서 정리한 내용입니다.

아래 내용에서 자세하게 볼 수 있겠지만 Spring Framework 4.2부터 스프링 이벤트의 사용이 간편해졌는데요.

기본적인 적용은 쉽고 간단하지만 실제 프로젝트에서는 @TransactionalEventListener 등의 조금 더 향상된 기능이 많이 사용되는데 해당 포스팅은 spring event 적용에 대한 기본적인 내용을 담았으며, 함께 사용되는 @TransactionalEventListener 어노테이션에 대한 내용은 추후 따로 포스팅하여 해당 글 하단에 링크하도록 하겠습니다.

 

 

 

1. 스프링 이벤트를 사용하는 이유와 장점

spring event

spring event를 사용하는 가장 주된 이유는 '서비스 간의 강한 의존성을 줄이기 위함'이라고 볼 수 있는데요.

 

예를 들어 어떤 상품을 주문하는 프로세스가 있고, 해당 프로세스는 내부적으로 주문을 처리한 뒤 푸시 메시지를 발송하고, 메일을 전송하는 과정을 거친다고 가정하겠습니다.

'주문 처리''푸시 메시지 발송', '메일 전송' 기능이 각각의 서비스(OrderService, PushService, MailService)에 구현되어 있을 경우, 아래 코드와 같이 주문 처리를 하는 OrderService에서 푸시 메시지 발송을 하는 PushService와 메일 전송을 하는 MailService에 대한 의존성을 주입받아 사용하게 되는데요.

 

@Service
public class OrderService {

    private final PushService pushService;
    private final MailService mailService;
    
    @Autowired
    public OrderService(PushService pushService, MailService mailService) {
        this.pushService = pushService;
        this.mailService = mailService;
    }
}

 

해당 예시는 간단한 경우지만 실제로 복잡한 도메인을 개발하게 되면 도메인 사이의 강한 의존성으로 인해 시스템이 복잡해지는 경우가 발생할 수 있다고 하며, 스프링 이벤트를 통해 이러한 도메인 간의 의존성을 줄일 수 있게 됩니다.

(강한 결합으로 인해 발생하는 유지보수 측면의 문제점도 줄일 수 있습니다.)

 

또한 이렇게 이벤트로 분리된 부분을 비동기 방식으로 처리하게 되면 전체 프로세스가 끝나는 시간도 짧아지게 된다는 성능 측면에서의 장점도 있는데요. 그러면 이어지는 내용을 통해 spring event의 구현 방법을 살펴보겠습니다.

 

 

 

2. 스프링 이벤트 구성 요소 및 동작 구현

spring event는 크게 'event class'와 이벤트를 발생시키는 'event publisher' 그리고 이벤트를 받아들이는 'event listener' 3가지 요소로 볼 수 있는데요.

 

2-1. event class

public class OrderedEvent extends ApplicationEvent {

    private String productName;

    public OrderedEvent(Object source, String productName) {
        super(source);
        this.productName = productName;
    }

    public String getProductName() {
        return productName;
    }
}

(Spring Framework 4.2 이전)

 

public class OrderedEvent {

    private String productName;

    public OrderedEvent(String productName) {
        this.productName = productName;
    }

    public String getProductName() {
        return productName;
    }
}

(Spring Framework 4.2부터)

 

'event class'는 이벤트를 처리하는데 필요한 데이터를 가지고 있으며, 기존에는 ApplicationEvent 클래스를 확장하여 사용하였지만 스프링 프레임워크 4.2 버전부터 위 예시와 같이 ApplicationEvent를 확장할 필요가 없어졌습니다.

 

 

2. event publisher

@Slf4j
@Service
public class OrderService {

    ApplicationEventPublisher publisher;

    public OrderService(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    public void order(String productName) {
        //주문 처리
        log.info(String.format("주문 로직 처리 [상품명 : %s]", productName));
        publisher.publishEvent(new OrderedEvent(productName));
        
        //4.2 버전 이전에서 event class가 ApplicationEvent를 구현하는 경우라면 
        //publisher.publishEvent(new OrderedEvent(this, productName));
    }
}

'event publisher'ApplicationEventPublisher 빈을 주입하여 publishEvent() 메서드를 통해 생성된 이벤트 객체를 넣어주면 되는데요.

 

ApplicationEventPublisher interface

앞의 'event class'에서도 언급하였지만 Spring Framework 4.2부터 ApplicationEventPublisher 인터페이스에서 다음과 같이 모든 객체를 이벤트로 하용하는 publishEvent(Object event) 메서드가 추가되었기 때문에 event class에서 ApplicationEvent를 상속받을 필요가 없어진 것입니다.

 

 

3. event listener

@Component
public class OrderedEventListener implements ApplicationListener<OrderedEvent> {

    @Override
    public void onApplicationEvent(OrderedEvent event) {
        ...
    }
}

(Spring Framework 4.2 이전)

 

@Slf4j
@Component
public class OrderedEventListener {

    @EventListener
    public void sendPush(OrderedEvent event) throws InterruptedException {
        log.info(String.format("푸시 메세지 발송 [상품명 : %s]", event.getProductName()));
    }

    @EventListener
    public void sendMail(OrderedEvent event) throws InterruptedException {
        log.info(String.format("메일 전송 [상품명 : %s]", event.getProductName()));
    }
}

(Spring Framework 4.2부터)

 

'event listener' 역시 스프링 프레임워크 4.2 버전을 기점으로 더 사용하기 쉽게 바뀌었는데요.

@EventListener 어노테이션을 통해 발생하는 이벤트를 캐치할 수 있으며, 기존과 같이 ApplicationListener<CustomEvent> 인터페이스를 구현하여 사용할 필요가 없어졌습니다.

 

 

 

3. 비동기 처리

스프링 이벤트는 기본적으로 '동기 방식'으로 동작하는데요. 때문에 이벤트를 처리하는데 오랜 시간이 걸리면 전체 프로세스가 끝나는 시간이 그만큼 길어지게 됩니다.

따라서 경우에 따라서는 이벤트를 비동기 처리하는 것이 좋을 수 있는데, 이벤트 비동기 처리 방식에 대해서 살펴보겠습니다.

 

@EnableAsync
@SpringBootApplication
public class EventApplication {

	public static void main(String[] args) {
		SpringApplication.run(EventApplication.class, args);
	}
}

(main 메서드가 있는 Application class)

 

@Slf4j
@Component
public class OrderedEventListener {

    @Async
    @EventListener
    public void sendPush(OrderedEvent event) throws InterruptedException {
        Thread.sleep(2000);  //2초
        log.info(String.format("푸시 메세지 발송 [상품명 : %s]", event.getProductName()));
    }

    @Async
    @EventListener
    public void sendMail(OrderedEvent event) throws InterruptedException {
        Thread.sleep(3000);  //3초
        log.info(String.format("메일 전송 [상품명 : %s]", event.getProductName()));
    }
}

(EventListener class)

 

비동기 처리도 어노테이션을 이용하여 간단하게 설정할 수 있는데요.

@EnableAsync 어노테이션을 통해 비동기를 사용하겠다고 선언하고, 비동기로 동작하고자 하는 메서드에 @Async 어노테이션을 설정하면 됩니다.

 

 

@Slf4j
@RequiredArgsConstructor
@RestController
public class OrderController {

    private final OrderService orderService;

    @GetMapping("/order/{productName}")
    private void order(@PathVariable String productName) {
        orderService.order(productName);
        log.info("주문이 완료되었습니다.");
    }
}

(OrderController)

 

동기 방식으로 작동되었을 때
비동기 방식으로 작동되었을 때

 

여기까지가 spring event를 적용하는 기본적인 방법이며, 이어지는 포스팅에서는 @EventListener 어노테이션보다 확장된 기능을 가진 @TransactionalEventListener 어노테이션의 사용 방법에 대해서 살펴보겠습니다.

 

 

 

< @TransactionalEventListener 사용 방법 포스팅 >

2023.03.16 - [Programming/Spring Boot] - Spring Event, @TransactionalEventListener 사용하기

 

 

< 참고 자료 >

https://shinsunyoung.tistory.com/88

https://wisdom-and-record.tistory.com/133

https://www.baeldung.com/spring-events

 

< github 소스 코드 >

https://github.com/JianChoi-Kor/spring-event