스프링 이벤트 3 - 이벤트 부가 기능 적용

아래 예제는 github에 소스를 올려놓았습니다.


1. 순서 적용

순차적으로 진행되는 메서드의 경우 그 순서를 알 수 있지만 이벤트 리스너의 경우 정해진 순서를 알 수가 없다.
(기본적으로 어떤 순서로 이벤트 리스너가 등록되는지는 더 찾아봐야할 것 같다.)
하지만 @Order 어노테이션을 통해 순서를 명시적으로 지정해 줄 수 있다.
아래는 EventListener 어노테이션의 Java doc에서 발췌한 것이다.

It is also possible to define the order in which listeners for a certain event are invoked. To do so, add Spring’s common @Order annotation alongside this annotation.

사용법은 @EventListener가 달린 메서드에 @Order를 추가해주면 된다.
Order의 값이 낮을 수록 우선순위가 높은 것이다.

이전 포스팅에서 완성한 애플리케이션을 실행하면 다음과 같이 EmailSender -> KakaoTalkSender 순으로 동작한다.

결제 내역 알림의 순서는 서비스에 크게 중요한건 아니지만 예제를 위해서
이메일보다 카톡을 먼저 보내야한다고 해보자.
그러면 앞서 말했듯이 EmailSender.sendNotificationKakaoTalkSender.sendNotification@Order를 추가해준다.
그리고 카톡을 먼저 보내야하기 때문에 이메일보다 Order 값이 낮아야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class EmailSender implements OrderNotifier {

@Override
@org.springframework.core.annotation.Order(2)
@EventListener // 이벤트 리스닝 기능 활성화
public void sendNotification(final OrderedEvent orderedEvent) {
Order order = orderedEvent.getOrder();

System.out.printf("send email to %s - order (%s: %d)%n",
order.getOrderer(), order.getProduct(), order.getPrice());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class KakaoTalkSender implements OrderNotifier {

@Override
@org.springframework.core.annotation.Order(1)
@EventListener // 이벤트 리스닝 기능 활성화
public void sendNotification(final OrderedEvent orderedEvent) {
Order order = orderedEvent.getOrder();

System.out.printf("send kakaotalk to %s - order (%s: %d)%n",
order.getOrderer(), order.getProduct(), order.getPrice());
}
}

여기서 우리는 주문으로 Order를 사용하기 때문에 패키지까지 달려있다.

다시 main method를 실행하면 아래와 같이 KakaoTalkSender -> EmailSender 순으로 동작한다.

2. 플래그 적용

만약 해당 사용자가 이메일 수신을 거부 했다면 보내면 해당 유저는 EmailSender.sendNotification 이벤트 리스너가 동작하면 안될 것이다.
그런 경우 플래그 값을 적용하여 이벤트 리스너 동작을 제어할 수 있다.

OrderedEvent에 이메일, 카톡에 대한 플래그를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class OrderedEvent {
private final Order order;
private final boolean kakaoTalkNotification;
private final boolean emailNotification;

public OrderedEvent(final Order order, final boolean kakaoTalkNotification, final boolean emailNotification) {
this.order = order;
this.kakaoTalkNotification = kakaoTalkNotification;
this.emailNotification = emailNotification;
}

public Order getOrder() {
return order;
}

public boolean isKakaoTalkNotification() {
return kakaoTalkNotification;
}

public boolean isEmailNotification() {
return emailNotification;
}
}

그리고 이벤트를 발급하는 OrderService에서 이 플래그값을 적용하면 된다.
예제로 카톡 알림을 보내고 이메일은 알림을 보내지 않는 것으로 하겠다.

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
32
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;

public OrderService(final ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}

public Order order(final String user, final OrderRequest orderRequest) {
Order order = doOrder(user, orderRequest);

if (order.isFailed()) {
throw new IllegalStateException("Order failed");
}

// eventPublisher.publishEvent(new OrderedEvent(order)); // 이벤트 발급
eventPublisher.publishEvent(new OrderedEvent(order, true, false)); // flag 적용

return order;
}

private Order doOrder(final String user, final OrderRequest orderRequest) {
String product = orderRequest.getProduct();
int price = orderRequest.getPrice();

if (user.isEmpty()) {
return Order.fail(user, product, price);
}

return Order.success(user, product, price);
}
}

이제 이벤트 리스너인 EmailSenderKakaoTalkSender에서 받은 이벤트의 플래그값을 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class EmailSender implements OrderNotifier {

@Override
@org.springframework.core.annotation.Order(2)
@EventListener
public void sendNotification(final OrderedEvent orderedEvent) {
if (!orderedEvent.isEmailNotification()) {
System.out.println("email won't be sent");
return;
}

Order order = orderedEvent.getOrder();

System.out.printf("send email to %s - order (%s: %d)%n",
order.getOrderer(), order.getProduct(), order.getPrice());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class KakaoTalkSender implements OrderNotifier {

@Override
@org.springframework.core.annotation.Order(1)
@EventListener
public void sendNotification(final OrderedEvent orderedEvent) {
if (!orderedEvent.isKakaoTalkNotification()) {
System.out.println("kakaotalk won't be sent");
return;
}

Order order = orderedEvent.getOrder();

System.out.printf("send kakaotalk to %s - order (%s: %d)%n",
order.getOrderer(), order.getProduct(), order.getPrice());
}
}

이제 main method를 실행하면 카톡은 보내고 이메일을 보내지 않는 것을 확인할 수 있다.

이렇게 메서드 내에서 직접 값을 받아와 핸들링할 수 있고,
또는 SpEL을 사용해서 지정해줄 수 있다.
@EventListenercondition 속성에 SpEL을 적용하면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class EmailSender implements OrderNotifier {

@Override
@org.springframework.core.annotation.Order(2)
@EventListener(condition = "#orderedEvent.emailNotification") // SpEL 적용
public void sendNotification(final OrderedEvent orderedEvent) {
Order order = orderedEvent.getOrder();

System.out.printf("send email to %s - order (%s: %d)%n",
order.getOrderer(), order.getProduct(), order.getPrice());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class KakaoTalkSender implements OrderNotifier {

@Override
@org.springframework.core.annotation.Order(1)
@EventListener(condition = "#orderedEvent.kakaoTalkNotification") // SpEL 적용
public void sendNotification(final OrderedEvent orderedEvent) {
Order order = orderedEvent.getOrder();

System.out.printf("send kakaotalk to %s - order (%s: %d)%n",
order.getOrderer(), order.getProduct(), order.getPrice());
}
}

다시 main method를 실행하면 아래와 같이 KakaoTalkSender 이벤트 리스너만 실행되는 것을 확인할 수 있다.


IntelliJ IDEA를 사용하면 SpEL 자동완성을 지원한다!


@EventListenercondition을 사용하는게 깔끔하지만 명시적으로 코드 상에 표기하는 걸 선호할 수도 있다.
하지만 값을 직접 받아서 핸들링 하는 경우 플래그 값에 대해 구체적인 기능을 지정할 수 있기 때문에 상황에 맞게 선택하자.

3. @TransantionalEventLisnter

이벤트 리스너 포스팅을 시작하면서 이런 문장을 사용했다.

알림에서 문제가 나면 연쇄적으로 결제 기능에도 문제가 생길 수 있다.

하지만 지금은 알림에서 문제가 나면 component를 직접 의존하던 것과 마찬가지로 결제 기능에 오류가 난다.
KakaoSender에서 오류가 RuntimeException이 발생할 경우 OrderService에서도 해당 Exception에 영향을 직접적으로 받는다.

결제 기능의 동작 완료을 확인하기 위해 마지막에 로그를 추가하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class OrderService {
// ...

public Order order(final String user, final OrderRequest orderRequest) {
// ...

eventPublisher.publishEvent(new OrderedEvent(order, true, false)); // flag 적용

System.out.println("정상적으로 주문을 마쳤습니다.");
return order;
}

// ...
}

그리고 KakaoSender.sendNotification에서 RuntimeException을 발생시키자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class KakaoTalkSender implements OrderNotifier {

@Override
@org.springframework.core.annotation.Order(1)
@EventListener(condition = "#orderedEvent.kakaoTalkNotification")
public void sendNotification(final OrderedEvent orderedEvent) {
throw new RuntimeException("카톡 연동에 문제가 생겼습니다.");

// Order order = orderedEvent.getOrder();
//
// System.out.printf("send kakaotalk to %s - order (%s: %d)%n",
// order.getOrderer(), order.getProduct(), order.getPrice());
}
}

그리고 main method를 실행하면 아래와 같이 RuntimeException에 의해 정상적으로 주문을 마쳤습니다.가 출력되지 않는다.

이러면 일부러 스프링의 이벤트 기능을 통해 결합도를 낮춘 의미가 없다고 생각된다.
단순히 직접 의존하지 않는 것 뿐이지 중간에 이벤트 퍼블리셔 - 이벤트 리스너라는 단계만 늘어난 것이다.

하지만 스프링은 @TransactionalEventListener를 통해 기능적으로도 결합도를 낮출 수 있는 기능을 제공한다.

이를 위해 pom.xml에 dependency를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

(사실 spring-tx 만을 dependency로 추가하여 예제를 만드려고 했지만 원하는대로 동작하지 않아서 JPA를 사용했다…)

그리고 이제 @EventListener@TransactionalEventListener로 변경한 후 애플리케이션을 실행해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class KakaoTalkSender implements OrderNotifier {

@Override
@org.springframework.core.annotation.Order(1)
// @EventListener(condition = "#orderedEvent.kakaoTalkNotification")
@TransactionalEventListener(condition = "#orderedEvent.kakaoTalkNotification")
public void sendNotification(final OrderedEvent orderedEvent) {
throw new RuntimeException("카톡 연동에 문제가 생겼습니다.");

// Order order = orderedEvent.getOrder();
//
// System.out.printf("send kakaotalk to %s - order (%s: %d)%n",
// order.getOrderer(), order.getProduct(), order.getPrice());
}
}

그러면 놀랍게도 아무런 Exception도 발생하지 않는다.

사실 @TransactionEventListener@Transactional이 붙은 메서드의 이벤트 퍼블리셔를 통해 발급된 이벤트를 리스닝하기 때문에 아무런 이벤트 리스너가 동작하지 않은 것이다.

@Transactional을 붙이고 다시 실행시켜보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class OrderService {
// ...

@Transactional
public Order order(final String user, final OrderRequest orderRequest) {
Order order = doOrder(user, orderRequest);

if (order.isFailed()) {
throw new IllegalStateException("Order failed");
}

eventPublisher.publishEvent(new OrderedEvent(order, true, false)); // flag 적용

System.out.println("정상적으로 주문을 마쳤습니다.");
return order;
}

// ...
}

그럼 위와 같이 이벤트 리스너의 오류와는 상관없이 그대로 진행되는 것을 확인할 수 있다.

@TransactionEventListener를 통해 결제알림의 결합도를 더욱 낮출 수 있다.


이로서 스프링의 이벤트 기능 알아보는 포스팅을 마치겠다.

Share