brunch

Spring @EventListener

스프링부트 이벤트 구현

by 기술블로그

이 글은, 스프링부트에서 제공하는 @EventListener에 대해서 소개한다.


목차

1. Overview : 글 소개

2. Spring Event(1) : 스프링 4.2 이전 버전에서의 이벤트

3. Spring Event(2) : 스프링 @EventListener

4. Spring ApplicationEvent : 스프링 부트에서 제공하는 이벤트


스프링 개발 경험이 많은 개발자는 지루한 이 글을 전부 읽지 말고,

@EventListener 에 대해서 소개하는 3장부터 읽는 것을 추천한다.



1.Overview

복잡한 도메인을 개발하다 보면, 도메인 사이에 강한 의존성으로 인해서 시스템이 복잡해지는 경우가 발생한다. 이 글에서는 복잡한 도메인 의존성을 줄이기 위한 방법 중 이벤트에 대해서 소개한다.


의존성

샘플코드는 상품 주문 도메인에 대한 사례이다. Order라는 이름의 주문 모델은, ChargeService 라는 응용 서비스를 사용해서 결제를 요청한다. (주문 도메인에서는 결제 처리 기능을 호출하게 된다.)

스크린샷 2020-06-07 오전 10.15.49.png

상품 상태에 대한 정의는 아래와 같다.

스크린샷 2020-06-07 오후 12.15.10.png

ChargeService 는 결제 로직을 수행하는 응용서비스이다.

스크린샷 2020-06-07 오후 12.12.23.png

Order 클래스의 pay 메서드에서 결제를 요청하는데, ChargeService 클래스의 charge() 메서드를 실행한다. startPay(), completedPay() 메서드에서는 상품 상태를 변경한다. Order 도메인에서 상품 상태에 대한 변경 역할을 담당한다.

스크린샷 2020-06-06 오후 11.36.45.png

상품 상태를 변경하는 역할은 Order 의 역할이 맞다. 하지만, 결제 로직은 Order 모델의 핵심 역할이 아니다. 결제는 ChargeService 에서 담당하며, 결제를 처리하는 응용서비스를 주입받아서 실행한다.


스크린샷 2020-06-07 오후 12.16.49.png

주문을 담당하는 Order에서 결제 로직을 실행하기 때문에, 주문 로직과 결제 로직이 섞여 있다.


어떻게 하면 의존성을 줄일 수 있을까?


이 글에서는, 도메인 모델 사이의 의존성을 줄일 수 있는 방법으로 이벤트를 소개한다.


참고로 이 글은 최범균님의 "DDD Start 도메인 주도 설계 구현과 핵심 개념 익히기"라는 책의 사례와 매우 유사하지만, 책의 내용을 그대로 인용하지 않았다. 해당 책은 DDD 입문자에게 괜찮은 책이지만 현재는 품절되었다. 회사 책장에 한 권씩은 굴러다니고 있을 테니, 찾아서 읽어보기를 바란다.



2. Spring Event(1)

이번 장에서는 스프링 4.2 이전 버전에서의 스프링 이벤트에 대해서 소개한다. 스프링 4.2 버전부터 @EventListener 어노테이션을 사용할 수 있으며, 해당 방법은 Spring Event(2)에서 확인하길 바란다.


Gradle Dependencies

생략


패키지

샘플 코드의 패키지 레이어는 아래와 같다.

스크린샷 2020-06-07 오전 1.09.08.png


이벤트, 이벤트 리스너

ApplicationEvent를 상속받아서 Event 객체를 정의하자.

스크린샷 2020-06-07 오전 12.49.30.png

이벤트 리스너는 ApplicationListener 인터페이스를 구현해야 한다.

스크린샷 2020-06-07 오전 12.48.48.png

이벤트가 발생했을 때 수신처리하는 구문은, onApplicationEvent 메서드에 작성한다. 리스너는 결제 서비스를 처리해야하기 때문에, ChargeService 응용서비스에 대한 의존성을 갖는다.


Order 도메인 모델에서는 이벤트를 발생시킨다. ApplicationEventPublisher를 사용해서 이벤트를 발생시킬 수 있는데, 위에서 정의했던 ChargeRequestEvent 객체를 생성해서 발행해야 한다.

스크린샷 2020-06-07 오전 12.58.54.png

정리해보면, Order 도메인에서는 ChargeService를 직접 실행하지 않는다. Order에서는 이벤트 객체를 전달하며, 이벤트를 핸들링하는 리스너에서 결제 로직을 실행하게 된다. 즉, Order와 ChargeService 두 클래스 사이에 의존성이 약하다.

스크린샷 2020-06-07 오후 12.37.31.png

OrderService(응용서비스)에서는 Order 도메인 모델의 pay() 메서드를 실행하면서 ApplicationEventPublisher를 전달하는 방식으로 구현하였다.

스크린샷 2020-06-07 오전 1.05.30.png


이 글을 보는 개발자분들 중에서, publisher를 Order 도메인 모델에 전달하는 방법이 부자연스럽다고 생각할 수 있다. 다른 방법으로는, OrderService에서 publisher를 직접 사용해서 이벤트를 직접 발생시키는 방법도 가능하다. 단, 이 경우에는 도메인 모델에 대한 상세 로직이 응용 서비스 레벨로 나오게 된다.

스크린샷 2020-06-07 오전 1.11.51.png

어떤 방식이 좋은지 허접한 개발자인 필자는 잘 모르겠다. 더 좋은 방법이 있다면 부족한 개발자인 필자에게 꼭 알려주길 바란다. 어쨌든, 이 글은 스프링 Event에 대한 주제이므로, 상세하게 설명하지는 않겠다.


중요한 사실은, publisher 객체의 publishEvent 메서드를 실행해서 이벤트를 발생시킬 수 있다는 점이다. 이때, 이벤트 객체인 ChargeRequestEvent를 생성해서 전달해야 한다.

스크린샷 2020-06-07 오후 12.43.15.png


검증

테스트 코드를 실행하였다.


스크린샷 2020-06-07 오후 7.31.41.png


스크린샷 2020-06-07 오후 7.32.04.png


샘플코드

https://github.com/sieunkr/spring-eventlistener/tree/master/spring-application-event




스프링 4.2부터 사용 가능한 @EventListener를 사용해서 코드를 개선해보자.


3. Spring Event(2) - @EventListener

스프링 4.2부터 사용 가능한 @EventListener 어노테이션에 대해서 알아보자.


Gradle Dependencies

생략


Event, EventListener

위에서 소개했던 샘플 코드의 방법(ApplicationEvent, ApplicationListener를 상속, 구현)할 필요가 없다. Event를 정의할 때 ApplicationEvent를 상속받을 필요가 없다.

스크린샷 2020-06-07 오전 1.25.02.png

이벤트 리스너는 ApplicationListener를 구현할 필요가 없고, @EventListener 어노테이션을 메서드 상단에 선언해주면 이벤트 리스너로 등록이 된다. 매개변수에 Event 클래스를 정의하면 해당 클래스 이벤트가 발생했을 때 이벤트를 수신해서 처리할 수 있다.

스크린샷 2020-06-07 오전 1.26.07.png

아주 심플하다.


Order

같은 이벤트에 대한 수신을 2개 이상의 핸들러 메서드에 정의할 수 있다. ChargeRequestEvent 이벤트가 발생하면 선언된 모든 메서드가 실행된다. 이 경우에, Order 어노테이션을 사용해서 메서드 실행 순서를 정의할 수 있다.

스크린샷 2020-06-07 오전 1.30.20.png


Async

이벤트 리스닝을 비동기로 구현해보자. 비동기 메서드가 동작할 수 있도록 최상단 클래스에 @EnableAsync 어노테이션을 선언한다.

스크린샷 2020-06-07 오전 1.32.19.png

비동기로 실행하길 원하는, 메서드에 @Async 어노테이션을 선언한다.

스크린샷 2020-06-07 오전 1.33.04.png

비동기에 대해서는 필자의 예전 글을 꼭 읽어보길 바란다.

https://brunch.co.kr/@springboot/267

https://brunch.co.kr/@springboot/401


느슨한 결합

이해를 돕기 위해서, 필자가 간단하게 그림을 그렸다.

스크린샷 2020-06-07 오후 1.38.36.png

이벤트 발행하는 주체는 Order 도메인이다. Order 도메인에서 publisher 객체의 publishEvent 메서드로 이벤트를 발생시킨다. 이때, ChargeRequestEvent 객체를 전달한다. 스프링의 ApplicationContext에서 이벤트 채널의 역할을 수행하며, @EventListener 어노테이션으로 선언한 메서드에서 ChargeRequestEvent 이벤트를 받아서 처리하게 된다.


샘플코드

https://github.com/sieunkr/spring-eventlistener/tree/master/spring-event-listener



4. SpringApplicationEvent

스프링에서 기본적으로 제공하는 이벤트에 대해서 아주 간단하게 알아보자.


SpringApplicationEvent

ApplicationEvent를 상속받는 SpringApplicationEvent라는 클래스를 알아보자.

스크린샷 2020-06-07 오전 1.42.02.png

SpringApplicationEvent 클래스는 스프링에서 동작하는 기본적인 이벤트를 정의하는 클래스이다. 해당 클래스를 상속받아서 정의한 이벤트는 아래와 같다.


- ApplicationContextInitializedEvent

- ApplicationEnvironmentPreparedEvent

- ApplicationPreparedEvent

- ApplicationReadyEvent

- ApplicationStartedEvent

- ApplicationFailedEvent

- ApplicationStartingEvent


스프링 애플리케이션이 동작하면 해당 이벤트를 자동으로 발생시켜 줄 것이다. ApplicationStartedEvent 이벤트를 수신하는 메서드를 아래와 같이 작성해보자.

스크린샷 2020-06-07 오전 9.55.57.png

스프링 내부 로직에서 ApplicationStartedEvent 이벤트를 발생시키는데, 해당 리스너는 애플리케이션이 처음 실행할 때 작동하게 된다.


Publisher

그렇다면, 이벤트는 어디서 발생시켜주는 걸까?

스프링부트를 제일 먼저 실행하는 run 메서드를 살펴보자.

스크린샷 2020-06-07 오전 9.59.10.png

SpringApplication 클래스의 run 메서드를 유심히 살펴보면, listeners.started 메서드가 있다.

스크린샷 2020-06-07 오전 10.02.50.png
스크린샷 2020-06-07 오전 10.01.30.png

메서드를 따라가 보면, publishEvent 해주는 구문을 찾을 수 있다.

스크린샷 2020-06-07 오전 10.03.25.png
스크린샷 2020-06-07 오전 10.03.16.png



어쨋든,

스프링이 처음 실행할 때 비즈니스 로직을 추가하고 싶다면, 스프링의 SpringApplicationEvent를 수신하는 이벤트 리스너를 구현하면 된다.


자세한 건 생략하겠다... 각자 알아서 공부하길 바란다.


글 마무리

이번 글에서는 스프링에서 Event를 사용하는 방법에 대해서 아주 간단하게 소개하였다. 실무에서 꽤 많이 사용하는 기능인데, 의외로 모르는 개발자가 꽤 있는 것 같다. 조금이라도 도움이 되었기를 바라며 글을 마치겠다.

keyword
매거진의 이전글Spring Boot @Async 어떻게 동작하는가?