개요
Cabi 서비스에서 메일과 슬랙 알림을 연동하기 위해, 스프링에서 사용되는 이벤트에 대해 알아보고, 적용해보고자 한다.
이벤트란?
컴퓨팅에서 이벤트란 프로그램에 의해 감지되고 처리될 수 있는 동작이나 사건을 말한다.
일반적으로 이벤트 기반 시스템은 프로그램에서 처리해야 할 비동기 외부 활동이 있을 때 사용된다.
스프링에서 이벤트를 사용하는 방법
기본적으로 이벤트를 사용하기 위해서, 다음과 같은 구성을 갖는다.
정보를 담는 Event 클래스 - Event를 Publish하는 Publisher - Event를 수신하고, 처리하는 Listener
코드로 구현하면 다음과 같이 작성해볼 수 있다.
Event 클래스
/**
* 대여 시작 이벤트 클래스
*/
@ToString
@Getter
public class LentStartEvent {
/**
* 메일링 및 알림을 위한 정보들을 가집니다.
*/
private final String name;
private final String email;
private final Date expiredAt;
private final Location location;
private final Integer visibleNum;
protected LentStartEvent(String name, String email, Date expiredAt, Location location,
Integer visibleNum) {
this.name = name;
this.email = email;
this.expiredAt = expiredAt;
this.location = location;
this.visibleNum = visibleNum;
}
/**
* 대여 시작 이벤트를 생성합니다.
* <p>
* 이벤트 생성에서 익셉션을 발생시키면 서비스 로직에 영향을 주므로,
* <br>
* 이벤트 리스너에서 따로 처리하도록 합니다.
*
* @param name 대여자 이름
* @param email 대여자 이메일
* @param expiredAt 대여 만료일
* @param location 대여한 캐비넷 위치 정보
* @return 이벤트
*/
public static LentStartEvent of(String name, String email, Date expiredAt, Location location,
Integer visibleNum) {
return new LentStartEvent(name, email, expiredAt, location, visibleNum);
}
}
Java
복사
값 검증에 대한 예외처리가 필요할 수 있으나, 이벤트 객체를 생성하여 publish하는 곳은 비즈니스 로직이 수행되는 서비스인 경우가 대다수 일 것이므로, 이 경우에 이벤트 생성시에 exception을 발생시키면 비즈니스 로직이 롤백이 되버리는 불상사가 발생할 수 있으므로, listener에서 핸들링을 해줄 때 처리해주는 편이 좋을 것 같다.
원하는 핸들링을 위한 정보들을 받아서 생성하고, publish하면 된다.
이전의 버전에서는 특정 인터페이스들을 상속 받아서 사용하고.. 복잡했었지만 지금은 단순히 이렇게 만들고 설정하면 바로 사용할 수 있다. 스프링 이거 미친거 아니야?
Publisher 사용하기
/**
* 예시를 위한 서비스입니다. 메일링, 알림 등을 구현하는 서비스라고 가정하고 테스트 용으로 작성했습니다.
*/
@Service
@RequiredArgsConstructor
@Transactional
public class EventTestService {
// 이벤트를 발행할 수 있도록 ApplicationEventPublisher를 주입받습니다 - 스프링에서 제공하는 기본 빈입니다.
private final ApplicationEventPublisher publisher;
// 이벤트를 핸들링하는(메일링, 알람 수행) 가짜 메서드입니다.
public void sendMail(LentStartEvent event) {
System.out.println("Service : 아래 정보에 따라 메일을 날리겠다!!\n" + event);
}
// 이벤트를 발행하는(대여 완료) 가짜 메서드입니다. 테스트를 위해 Listen과 Publish가 같이 있지만, 실제 구현 시에는 분리됩니다.
public void evokeEvent() {
publisher.publishEvent(
LentStartEvent.of(
"까비",
"ccabi@cabi.oopy.io",
DateUtil.getNow(),
Location.of("까비네 집", 2, "방구석"),
42));
}
}
Java
복사
간단한 예시를 위해서 부득이하게 TestService에 Listener가 원하는 로직을 위해 호출하는 sendMail이라는 가짜 메서드와, Event를 publish하는 evokeEvent를 작성했다.
스프링에서 기본적으로 제공하는 빈인 ApplicationEventPublisher(인터페이스)를 주입받으면 바로 publish 할 수 있다(스프링 폼 미쳤다).
원하는 정보를 담은 Event 객체를 생성하여 Publish한다.
Listener로 원하는 로직 처리하기
/**
* 대여 도메인 이벤트 리스너입니다.
*/
@Component
@RequiredArgsConstructor
public class LentEventListener {
private final EventTestService eventTestService;
/**
* {@link TransactionalEventListener}(phase = TransactionPhase.AFTER_COMMIT) <- 롤백되지 않은 커밋 후에
* 이벤트를 발생시킵니다. 테스트 하기 힘들어서 현재는 {@link EventListener}로 해놓았지만, 트랜잭션 롤백시에 이벤트가 발생하면 안 되는 곳들에서 잘
* 사용해야 합니다.
*/
@EventListener // <- publish되면 무조건 listen 합니다.
public void handleLentStartEvent(LentStartEvent event) {
System.out.println("Listener : 이벤트 발생!!");
eventTestService.sendMail(event);
System.out.println("Listener : 메일 보냈다!");
}
}
Java
복사
지금은 EventTestService를 주입 받았지만, 실제 구현에서는 원하는 서비스(메일링이든 알림이든)를 주입받고 사용하면 된다. @EventListener 어노테이션을 메서드에 달아놓으면, Publisher가 발행한 이벤트에 따라서, 메서드의 매개변수(이벤트 클래스)를 인식하여 처리한다.
한 개의 이벤트에 대해서 여러 개의 Listener 메서드를 사용할 수 있고(그냥 여러 개 작성하면 된다), @Order 어노테이션을 통해 그 순서 또한 정할 수 있다.
트랜잭션(@Transactional)을 사용하는 경우에 따라서, 원치 않는 롤백임에도 메일을 보낸다든지 하면 원하는 동작이 아니게 된다. 이를 위해서, publish되는 부분의 트랜잭션 결과에 따른 listen이 가능하다.
@EventListener
•
매개변수에 있는 이벤트 클래스가 publish 되기만 하면, 메서드를 실행한다.
@TransactionalEventListener
(phase = TransactionPhase.AFTER_COMMIT) - 트랜잭션이 commit 되었을 때 메서드를 실행한다.
(phase = TransactionPhase.ROLLBACK) - 트랜잭션이 rollback 되었을 때 이벤트를 실행한다.
(phase = TransactionPhase.AFTER_COMPLETION) - 트랜잭션이 completion(commit 또는 rollback) 되었을 때 이벤트 실행한다.
(phase = TransactionPhase.BEFORE_COMMIT) - 트랜잭션이 commit 되기 전에 이벤트를 실행한다.
위 어노테이션을 잘 사용해서, 원하는 방식으로 이벤트 핸들링을 할 수 있을 것이다.
테스트 작성하기
/**
* 이벤트 리스너 테스트
* <p>
* {@link RecordApplicationEvents}를 통해 테스트에서 이벤트를 기록하고, {@link ApplicationEvents}를 통해 이벤트를 조회할 수
* 있습니다.
*/
@SpringBootTest
@RecordApplicationEvents
@Transactional
class LentEventListenerTest {
@Autowired
EventTestService eventTestService;
@Autowired
ApplicationEvents applicationEvents; // IDE에서 오토와이어링 할 수 없다고 뜨지만 잘 됩니다.
@Test
void test() {
eventTestService.evokeEvent();
assertEquals(1,
applicationEvents.stream(LentStartEvent.class)
.filter(event -> event.getName().equals("까비"))
.count());
}
}
Java
복사
@RecordApplicationEvents라는 어노테이션을 통해 테스트에서 이벤트를 기록하고,
ApplicationEvents를 주입 받음으로써 테스트가 진행되는 동안 ApplicationContext에서 발행되는 이벤트들을 관리할 수 있다. RecordApplicationEvents와 ApplicationEvents는 같이 써야 사용이 가능하다.
이전에 만들어 놓은 EventTestService를 호출하면 다음과 같이 테스트가 잘 동작하는 것을 볼 수 있다.
정리
간단한 방식으로 구현할 수는 있지만, 외부 서비스를 이용해서 알림이나 메일을 보내는 방식에 대해서는 구조적인 고민이 더 필요할 것 같다. 이후에 슬랙 봇을 적용해보는 것으로..!