Skip to content

ApplicationEventPublisher 사용하기

Jeonghwa Heo edited this page Jun 4, 2023 · 2 revisions

이벤트 만들기

스프링 4.2이전엔 ApplicationEvent를 상속받아야한다.

  • 빈으로 등록되는게 아닌 원하는 이벤트를 담아 전송하는 클래스다.
  • 원하는 데이터가 있다면 실어서 보낼 수 도 있다.
package com.smart.user.domain;

import org.springframework.context.ApplicationEvent;

public class MailEvent extends ApplicationEvent {

  private String data;

  public MailEvent(Object source) {
    super(source);
  }

  public MailEvent(Object source, String data) {
    super(source);
    this.data = data;
  }

  public String getData() {
    return data;
  }
}

이벤트 발생 시키는 방법

ApplicationContext 가 발생시키는 방법을 가지고 있다.

ApplicationContext extends ApplicationEventPublisher

@FunctionalInterface
public interface ApplicationEventPublisher {
	default void publishEvent(ApplicationEvent event) {
		publishEvent((Object) event);
	}
}

AppRunner 클래스 생성

  • ApplicationRunner 구현
  • ApplicationContext를 주입 받아도되고ApplicationEventPublisher를 주입받는 것도 가능함.
package com.smart.mail.domain;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements ApplicationRunner {

  @Autowired
  ApplicationEventPublisher applicationEventPublisher;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    applicationEventPublisher.publishEvent(new MailEvent(this, "data"));
  }
}

이벤트 처리하는 방법

  • 이벤트 핸들러 생성 → 빈으로 등록이 되어야함
  • 스프링 4.2이전엔 ApplicationListener가 되어야함.
package com.smart.mail.domain;

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class MailEventHandler implements ApplicationListener<MailEvent> {

  @Override
  public void onApplicationEvent(MailEvent event) {
    System.out.println("이벤트를 받았다. "+event.getData()+"에게 메일을 전송하자.");
  }
}

스프링 컨테이너를 띄우면 이벤트가 발생한다.


스프링 4.2버전 이후라면 아래와 같이 코딩하자.

  1. 어떠한 스프링코드도 들어가있지 않은 이벤트 클래스
package com.smart.mail.domain;

public class MailEvent {

  private String data;

  private Object source; // 이벤트 소스를 받고싶다면 추가

  public MailEvent(Object source, String data) {
    this.source = source;
    this.data = data;
  }

  public Object getSource() {
    return source;
  }

  public String getData() {
    return data;
  }
}
  1. 애노테이션만 들어가있는 이벤트 핸들러
package com.smart.mail.domain;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class MailEventHandler{

  @EventListener // 이벤트를 처리하는 애노테이션. 메서드 이름 마음대로 해도됨.
  public void eventHandle(MailEvent event) {
    System.out.println("이벤트를 받았다. "+event.getData()+"에게 메일을 전송하자.");
  }
}

이벤트의 순서를 정하자.

@Order애노테이션을 사용하여 이벤트 우선순위를 설정함

Thread[main,5,main]
이벤트를 받았다. data에게 메일을 전송하자.
Thread[main,5,main]
다른 이벤트를 받았다. data에게 메일을 전송하자.
package com.smart.mail.domain;

import static java.lang.Thread.currentThread;

import org.springframework.context.event.EventListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
public class AnotherMailEventHandler {

  @EventListener
  @Order(Ordered.HIGHEST_PRECEDENCE + 2)
  public void eventHandle(MailEvent event) {
    System.out.println(currentThread().toString());
    System.out.println("다른 이벤트를 받았다. "+event.getData()+"에게 메일을 전송하자.");
  }
}
Thread[main,5,main]
이벤트를 받았다. data에게 메일을 전송하자.
Thread[main,5,main]
다른 이벤트를 받았다. data에게 메일을 전송하자.

비동기로 실행하고싶다

@Async 애노테이션을 사용한다.

  • 어떤 스레드가 실행시킬지 모르기 때문에 순서는 당연히 보장이 되지 않는다.
package com.smart.mail.domain;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;

@Component
@EnableAsync
public class AppRunner implements ApplicationRunner {

  @Autowired
  ApplicationEventPublisher applicationEventPublisher;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    applicationEventPublisher.publishEvent(new MailEvent(this, "data"));
  }
}

@EnableAsync : 여러스레드를 만들어 이벤트를 발생시킴.

package com.smart.mail.domain;

import static java.lang.Thread.currentThread;

import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class MailEventHandler{

  @EventListener
  @Async
  public void eventHandle(MailEvent event) {
    System.out.println(currentThread().toString());
    System.out.println("이벤트를 받았다. "+event.getData()+"에게 메일을 전송하자.");
  }
}
package com.smart.mail.domain;

import static java.lang.Thread.currentThread;

import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class AnotherMailEventHandler {

  @EventListener
  @Async
  public void eventHandle(MailEvent event) {
    System.out.println(currentThread().toString());
    System.out.println("다른 이벤트를 받았다. "+event.getData()+"에게 메일을 전송하자.");
  }
}
Thread[task-1,5,main]
다른 이벤트를 받았다. data에게 메일을 전송하자.
Thread[task-2,5,main]
이벤트를 받았다. data에게 메일을 전송하자.

참고 : 백기선 - 스프링 프레임워크 핵심 기술

트랜잭션 및 스레드 분리

  • 회원가입시 User를 테이블에 저장하는 트랜잭션과 메일발송에 대한 트랜잭션 및 스레드를 분리함으로써 회원가입 버튼을 누른 사용자는 메일발송이 끝날 때 까지 응답을 기다리지 않아도 된다.
@Service
public class UserService {

  private final ApplicationEventPublisher eventPublisher;
...

  @Transactional
  public Long join(UserSaveDto saveDto) {
    checkDuplicateEmail(saveDto.getEmail());
    checkDuplicateNickname(saveDto.getNickname());

    User user = saveDto.toEntity();
    String authCode = getAuthCode();
    userRepository.save(user);

    eventPublisher.publishEvent(new MailAuthEvent(user.getEmail(), authCode)); // 이벤트 호출
    return user.getUserId();
  }
...
}
@Component
public class MailEventHandler{

  private final MailService mailService;

  public MailEventHandler(MailService mailService) {
    this.mailService = mailService;
  }

  // 회원가입 트랜잭션과 분리하기 위해 Transaction을 새로 만들어줌.
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  @TransactionalEventListener
  // 성능을 위해 비동기로 처리한다(회원가입 스레드와 메일전송 스레드 분리)
  @Async
  public void eventHandle(MailAuthEvent event) {
    mailService.sendAuthMail(event.getEmail(), event.getAuthCode());
  }
}

참고 : 이벤트 기반, 서비스간 강결합 문제 해결하기 - ApplicationEventPublisher