Search
Duplicate

스프링에서 이벤트 구현하기

간단소개
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
Java
Spring
Scrap
태그
9 more properties

개요

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를 호출하면 다음과 같이 테스트가 잘 동작하는 것을 볼 수 있다.

정리

간단한 방식으로 구현할 수는 있지만, 외부 서비스를 이용해서 알림이나 메일을 보내는 방식에 대해서는 구조적인 고민이 더 필요할 것 같다. 이후에 슬랙 봇을 적용해보는 것으로..!

참고자료