[Spring] Event
1. Spring Event 란?
- Spring Event는 Spring Framework에서 제공하는 이벤트 기반의 프로그래밍 모델
- 이는 애플리케이션 내에서 발생하는 특정 이벤트에 대해 비동기적으로 반응할 수 있는 강력한 기능을 제공하며, Spring Event는 이벤트 발생자(Publisher)와 이벤트 리스너(Listener) 간의 느슨한 결합을 지원함
- 이는 모듈 간의 독립성을 유지하면서도 서로 통신할 수 있게 해주는 기능임
2. Spring 이벤트를 사용하는 이유와 장점
- 서비스 간의 강한 의존성을 줄이기 위함 (강한 결합으로 인해 발생하는 유지보수 측면의 문제점도 줄일 수 있음)
- 또한, 이벤트로 분리된 부분을 비동기 방식으로 처리하게 되면 전체 프로세스가 끝나는 시간도 줄일 수 있음
- 예를 들어 어떤 상품을 주문하는 프로세스가 있고, 해당 프로세스는 내부적으로 주문을 처리한 뒤 푸시 메시지를 발송하고, 메일을 전송하는 과정을 거친다고 가정할 때 '주문 처리', '푸시 메시지 발송', '메일 전송' 기능이 각각의 서비스로 구현되어 있을 경우 아래 코드와 같이 OrderService에서 PushService, MailService에 대한 의존성을 주입받아 사용
@Service
public class OrderService {
private final PushService pushService;
private final MailService mailService;
public OrderService(PushService pushService, MailService mailService) {
this.pushService = pushService;
this.mailService = mailService;
}
public OrderResponse order(OrderRequest request) {
// 주문 처리 로직
// 푸시 알람 발송 로직
this.pushService.sendPushAlarm(request.getUserName());
// 메일 알람 발송 로직
this.mailService.sendMail(request.getEmail());
}
}
- 위의 예시는 간단한 경우지만 실제로 복잡한 도메인을 개발하게 되면 도메인 사이의 강한 의존성으로 인해 시스템이 복잡해지는 경우가 발생, 스프링 이벤트를 통해 이러한 도메인 간의 의존성을 줄일 수 있음
3. Spring Event의 주요 개념
- Spring Event는 크게 'event class'와 이벤트를 발생시키는 'event publisher' 그리고 이벤트를 받아들이는 'event listener' 3가지 개념이 있음
3.1 이벤트 클래스 (Event Class)
- 이벤트 클래스는 이벤트를 처리하는데 필요한 데이터를 가지고 있으며, Spring에서 이벤트는 일반적으로 ApplicationEvent 클래스를 상속받아 정의됨
// (Spring Framework 4.2 이전)
public class OrderedEvent extends ApplicationEvent {
private String productName;
public OrderedEvent(Object source, String productName) {
super(source);
this.productName = productName;
}
public String getProductName() {
return productName;
}
}
- 기존에는 ApplicationEvent 클래스를 확장해서 사용했지만 스프링 프레임워크 4.2버전부터 아래의 예시와 같이 ApplicationEvent를 확장할 필요가 없어짐
// (Spring Framework 4.2부터)
public class OrderedEvent {
private String productName;
public OrderedEvent(String productName) {
this.productName = productName;
}
public String getProductName() {
return productName;
}
}
3.2 이벤트 발생자 (Event Publisher)
- 이벤트 발생자는 특정 상황에서 이벤트를 발생시키는 주체로서, Spring에서는 ApplicationEventPublisher 인터페이스를 통해 이벤트를 발행 (스프링 빈 내에서 ApplicationEventPublisher를 주입받아 이벤트를 발행)
@Slf4j
@Service // bean
public class OrderService {
// 이벤트 발행 주체
private final 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));
}
}
3.3 이벤트 리스너(Event Listener)
- 이벤트 리스너는 특정 이벤트가 발생했을 때 그것에 반응하는 컴포넌트로서, Spring에서는 @EventListener 어노테이션을 사용하여 이벤트 리스너를 정의할 수 있음
- 리스너 메서드에 @EventListener 어노테이션을 추가하고, 처리할 이벤트 유형을 메서드 매개변수로 받음
// (Spring Framework 4.2 이전)
@Component
public class OrderedEventListener implements ApplicationListener<OrderedEvent> {
@Override
public void onApplicationEvent(OrderedEvent event) {
...
}
}
- Spring Framework 4.2 이후 @EventListener 어노테이션을 통해 발생하는 이벤트를 캐치할 수 있으며, 기존과 같이ApplicationListener<CustomEvent> 인터페이스를 구현하여 사용할 필요가 없어짐
@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()));
}
}
4. 동기 vs 비동기 이벤트 처리
4.1 비동기 처리
- 기본적으로 Spring 이벤트는 동기적으로 처리되기 때문에, 이벤트가 발생하면 리스너들이 순차적으로 호출되는 형태를 가짐
- 비동기적 처리를 위해서는 @Async 어노테이션을 리스너 메서드에 추가할 수 있음, 이를 통해 리스너 메서드는 별도의 스레드에서 실행됨
@EnableAsync // @EnableAsync 어노테이션을 통해 비동기를 사용하겠다고 선언
@SpringBootApplication
public class EventApplication {
public static void main(String[] args) {
SpringApplication.run(EventApplication.class, args);
}
}
@Slf4j
@Component
public class OrderedEventListener {
@Async // 비동기 처리
@EventListener
public void sendPush(OrderedEvent event) throws InterruptedException {
log.info(String.format("푸시 메세지 발송 [상품명 : %s]", event.getProductName()));
}
@Async // 비동기 처리
@EventListener
public void sendMail(OrderedEvent event) throws InterruptedException {
log.info(String.format("메일 전송 [상품명 : %s]", event.getProductName()));
}
}
4.2 비동기 처리 시 고려사항
- 비동기 이벤트 처리에서는 리스너가 별도의 스레드에서 실행되므로, 메모리 관리와 스레드 풀 크기 등을 고려해야 함
- 특히, 많은 이벤트가 비동기로 처리되면 스레드 풀이 포화될 수 있기 때문에, 이를 방지하기 위해 스레드 풀 설정을 적절히 조정해야 함
- 스레드 풀 설정: @EnableAsync를 사용할 때, 커스텀 스레드 풀 설정을 통해 비동기 이벤트 처리의 성능을 최적화할 수 있음
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.initialize();
return executor;
}
}
5. Transaction Boundaries와 @TransactionalEventListener
5.1 Transaction Boundaries
- Spring Event는 트랜잭션의 경계를 존중
- 트랜잭션 내에서 이벤트를 발행할 경우, 트랜잭션이 성공적으로 커밋된 후에 이벤트가 리스너에게 전달됨
- 즉, 트랜잭션이 롤백되면 이벤트가 발생하지 않으므로, 트랜잭션의 무결성을 유지할 수 있음
- 이벤트가 트랜잭션이 끝나기 전에 발행되길 원할 경우, @TransactionalEventListener의 phase 속성을 BEFORE_COMMIT으로 설정할 수 있음, 이는 이벤트를 트랜잭션이 커밋되기 전에 발생시킬 수 있도록 함
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleBeforeCommit(OrderedEvent event) {
// 트랜잭션 커밋 전에 이벤트 처리
}
5.2 @TransactionalEventListener
- @TransactionalEventListener는 동작하는 메서드를 트랜잭션으로 묶어서 처리하는 경우 트랜잭션의 상태에 따라 발생하는 이벤트를 처리해주는 이벤트 리스너
- 그러므로 이벤트 처리가 필요한 로직에서 트랜잭션을 적용해야 하는 경우 @EventListener 어노테이션이 아닌 @TransactionalEventListener 어노테이션을 사용해야 함 (그러므로, 트랜잭션이 적용되지 않는다면 이벤트 리스너가 동작하지 않음)
- 특정 트랜잭션이 성공적으로 커밋된 후, 혹은 트랜잭션이 롤백되기 전에 이벤트를 처리하도록 설정할 수 있음
5.3 @TransactionalEventListener를 사용하는 이유
* 트랜잭션의 일관성 유지
- 트랜잭션이 성공적으로 커밋된 후에만 이벤트가 발생하므로, 트랜잭션 내에서의 작업이 확실히 완료된 후에 후속 작업을 수행할 수 있음
* 트랜잭션 실패 처리
- 트랜잭션이 롤백될 때만 이벤트를 발생시켜, 실패에 따른 특정 작업(예: 보상 트랜잭션, 롤백 알림 등)을 처리할 수 있음
public class Member {
private final MemberRepository memberRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void signup(MemberDto memberDto) {
//1. 회원가입 회원 정보 저장
memberRepository.save(new Member(memberDto.getId(), memberDto.getName()));
//2. 회원가입 축하 메일 전송 이벤트 발생
eventPublisher.publishEvent(new SavedMemberEvent(memberDto));
//3. 어떠한 사유로 인해 exception 발생
if (memberDto.getName().equals("master")) {
throw new RuntimeException("can not use this name.");
}
}
}
- @EventListener의 경우 publishEvent() 메서드가 호출되는 시점에서 바로 이벤트를 publishing 함
- 만약 위의 코드에서 다음과 같이 트랜잭션으로 묶인 signup() 메서드에서 '1. 회원가입 회원 정보 저장' 부분과 '2. 회원가입 축하 메일 전송 이벤트 발생' 부분이 정상적으로 동작한 뒤에 '3. 어떠한 사유로 인해 exception 발생' 부분에서 exception이 발생된다면, '1. 회원 정보 저장' 부분은 트랜잭션에 의해 롤백이 실행되지만, '2. 축하 메일 전송' 부분은 롤백이 되지 않는 상황이 발생하게 됨
- 이러한 경우가 발생하기 떼문에 트랜잭션이 적용되는 로직에서 이벤트 처리가 필요할 때는 @TransactionalEventListener가 사용됨
5.4 @TransactionalEventListener의 주요 속성
* phase
- 트랜잭션의 어느 단계에서 이벤트를 처리할지 지정할 수 있음
- 기본값은 AFTER_COMMIT로, 트랜잭션이 성공적으로 커밋된 후에 이벤트를 처리함
- 다른 옵션으로는 BEFORE_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION이 있음
@Slf4j
@Component
public class TransactionalOrderedEventListener {
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleBeforeCommit(OrderedEvent event) {
log.info("트랜잭션 커밋 전에 처리: {}", event.getProductName());
}
//
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(OrderedEvent event) {
log.info("트랜잭션 커밋 후에 처리: {}", event.getProductName());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleAfterRollback(OrderedEvent event) {
log.info("트랜잭션 롤백 후에 처리: {}", event.getProductName());
}
}
* fallbackExecution
- 트랜잭션이 존재하지 않을 때 이벤트를 처리할지 여부를 지정
- 기본값은 false이며, 트랜잭션이 없으면 이벤트가 처리되지 않고, true로 설정하면 트랜잭션이 없을 때도 이벤트가 처리됨
6. 이벤트 프로세싱의 우선순위
- Spring Event는 기본적으로 여러 리스너가 동일한 이벤트를 처리할 때, 처리 순서를 지정하지 않음
- 하지만, @Order 어노테이션을 사용하여 특정 리스너가 먼저 실행되도록 우선순위를 지정할 수 있음
@Component
public class HighPriorityListener {
@EventListener
@Order(1) // 첫 번째 순서
public void handleEvent(OrderedEvent event) {
// 가장 먼저 실행될 로직
}
}
@Component
public class LowPriorityListener {
@EventListener
@Order(2) // 두 번째 순서
public void handleEvent(OrderedEvent event) {
// 두 번째로 실행될 로직
}
}
7. EventListener의 조건부 실행
- @EventListener 어노테이션의 condition 속성을 사용하면, 특정 조건이 만족될 때만 리스너가 실행되도록 할 수 있음
- 예를 들어, 이벤트의 특정 필드 값이 특정 조건을 만족하는 경우에만 리스너가 실행되도록 할 수 있음
// productName이 SpecialProduct일 경우에만 이벤트 리스너 실행
@EventListener(condition = "#event.productName == 'SpecialProduct'")
public void handleSpecialProductEvent(OrderedEvent event) {
// 특정 조건에 따라 이벤트 처리
}
8. Spring Event의 활용 예
8.1 사용자 가입 처리
- 사용자가 가입하면 사용자 정보를 저장하고, 이메일을 보내는 등의 작업을 할 수 있음
- 이때 이벤트 기반으로 이메일 전송을 처리하게 되면 효율성 높아짐
8.2 캐시 갱신
- 데이터베이스의 특정 데이터가 변경되었을 때, 관련 캐시를 갱신하는 작업을 이벤트 리스너로 처리할 수 있음
8.3 비즈니스 로직의 분리
- 핵심 비즈니스 로직과 부가적인 작업(예: 로그 기록, 알림 전송 등)을 분리하여 관리할 수 있음