Spring에서 Event가 왜 필요한가 ?
Spring Event는 Spring의 3대요소 중 하나인 관점을 분리하기 위한 하나의 방법이다.
예를 들어 유저가 회원가입을 할 때 인증을 위한 이메일을 보내는 로직이 어디에 존재해야할까 고민해볼 수 있다.
@Service
@RequiredArgsConstructor
@Transactional
public class AccountService implements UserDetailsService {
private final AccountRepository accountRepository;
private final EmailService emailService;
public Account signUp(SignUpForm signUpForm) {
Account newAccount = saveNewAccount(signUpForm); // 컨트롤러에서 넘겨받은 DTO로 Entity 생성
sendSignUpConfirmMail(newAccount);// 이메일 인증 로직
return newAccount; // 회원가입 성공, 이후 컨트롤러에서는 회원가입 이후의 View or API 응답
}
public void sendSignUpConfirmMail(Account newAccount) {
// 회원 정보로 이메일 내용 만드는 로직
emailService.sendEmail(email);
}
}
물론 이런 방식으로 계정 서비스 내에서 이메일을 보내는 로직이 포함되게 할 수도 있지만, 회원 가입이라는 이벤트가 발생했을 때 해당 행동을 구독하고 있다가 다른 서비스에서 해당 로직을 처리해 서비스 간 의존성을 낮출 수 있게끔 해주는 것이 Srping Event이다.
스프링 이벤트의 요소, 동작
Event Class와 이벤트를 발생시키는 Event Publisher, 이벤트를 처리하는 Event Listener로 구성된다.
Event Class
스프링 4.2 이전에는 ApplicationEvent 클래스를 상속받아야 했지만 지금은 그냥 DTO처럼 사용하면 된다.
@Getter
@RequiredArgsConstructor
public class AccountCreatedEvent{
private final Account Account;
}
Event Publihser
기본적으로 스프링에서 제공해주는 ApplicationPublisher 인터페이스 구현체를 주입받아 EventClass를 publish한다.
@FunctionalInterface
public interface ApplicationEventPublisher {
default void publishEvent(ApplicationEvent event) {
publishEvent((Object) event);
}
/**
* Notify all <strong>matching</strong> listeners registered with this
* application of an event.
* <p>If the specified {@code event} is not an {@link ApplicationEvent},
* it is wrapped in a {@link PayloadApplicationEvent}.
* <p>Such an event publication step is effectively a hand-off to the
* multicaster and does not imply synchronous/asynchronous execution
* or even immediate execution at all. Event listeners are encouraged
* to be as efficient as possible, individually using asynchronous
* execution for longer-running and potentially blocking operations.
* @param event the event to publish
* @since 4.2
* @see #publishEvent(ApplicationEvent)
* @see PayloadApplicationEvent
*/
void publishEvent(Object event);
}
Spring 4.2 이후부터 ApplicationPublisher 인터페이스에 Object를 파라미터로 받는 메소드가 추가되어 이전의 ApplicationEvent를 상속받을 필요가 없다.
@Service
@RequiredArgsConstructor
@Transactional
public class AccountService implements UserDetailsService {
private final AccountRepository accountRepository;
private final ApplicationEventPublisher eventpublisher;
public Account signUp(SignUpForm signUpForm) {
Account newAccount = saveNewAccount(signUpForm); // 컨트롤러에서 넘겨받은 DTO로 Entity 생성
eventpublisher.publishEvent(new AccountCreatedEvent(newAccount); // 회원 생성 Event Publish
return newAccount; // 회원가입 성공, 이후 컨트롤러에서는 회원가입 이후의 View or API 응답
}
// AccountService 내의 EmailService 의존성 삭제
}
Event Listener
이벤트 리스너도 스프링 4.2부터 ApplicationListener 인터페이스 구현체가 아닌 별도의 빈에 등록되는 클래스의 메소드에 @EventListener 어노테이션으로 해결할 수 있다.
@Component // 4.2 이전
@Slf4j
@RequiredArgsConstructor
public class AccountEventListener implements ApplicationListener<AccountCreatedEvent>{
private final EmailService emailService;
@Override
public void onApplicationEvent(AccountCreatedEvent event) {
// 이메일 발송 로직
}
}
@Component
@Slf4j
@RequiredArgsConstructor
public class AccountEventListener {
private final EmailService emailService;
@EventListener
public void handleAccountCreatedEvent(AccountCreatedEvent accountCreatedEvent) {
// 이메일 발송 로직
}
}
비동기 처리
응답시간이 긴 외부 API 사용이나, 네트워크 I/O, 데이터베이스 쓰기 작업 등의 로직이 동기적으로 처리된다면 사용자는 서버로부터 응답이 올 때까지 로딩을 하는 불편한 상황이 만들어진다. 이벤트 처리 프로세스 시간이 오래걸리면서 즉각적은 피드백의 중요도가 낮을 때에는 컨트롤러 요청을 처리하는 쓰레드가 아닌 다른 쓰레드를 할당받아 처리하는 비동기처리를 할 수 있다.
서비스의 메소드에 @Transactional이 걸려있어도 이벤트 리스너를 비동기처리하면 해당 로직은 트랜잭션과 관계없이 따로처리
Async Configuration
@EnableAsync
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
OR
public interface AsyncConfigurer { // 구현체 사용
@Nullable
default Executor getAsyncExecutor() {
return null;
}
@Nullable
default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}
AsyncConfigurer 인터페이스 구현체를 사용하거나 루트 애플리케이션에 @EnableAsync를 통해 현재 프로젝트의 비동기 처리를 가능하게 한다.
<p>By default, Spring will be searching for an associated thread pool definition:
* either a unique {@link org.springframework.core.task.TaskExecutor} bean in the context,
* or an {@link java.util.concurrent.Executor} bean named "taskExecutor" otherwise. If
* neither of the two is resolvable, a {@link org.springframework.core.task.SimpleAsyncTaskExecutor}
별도의 설정을 하지 않고 루트에 @EnableAsync를 설정해 사용하면 기본적으로 SimpleAsyncTaskExecutor를 통해 비동기 처리를 진행하는데 해당 Executor는 비동기 요청이 생기면 쓰레드 생성 > 요청 > 삭제의 프로세스를 가지므로 이벤트 처리에 그리 적합하지 않기 떄문에 별도의 쓰레드풀을 미리 할당하기 위해 AsyncConfigurer 인터페이스 구현체를 사용하는 편이 좋다.
@Configuration
@EnableAsync // Spring에 Aysnc 사용을 등록
@Slf4j
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int processors = Runtime.getRuntime().availableProcessors();
log.info("process : {}", processors);
executor.setCorePoolSize(processors);
executor.setMaxPoolSize(processors * 2);
executor.setQueueCapacity(50);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("AsyncExecutor-");
executor.initialize();
return executor;
}
}
EventListener
@Component
@Slf4j
@RequiredArgsConstructor
@Async // 클래스에 붙이면 모든 메소드 비동기 처리, 메소드 별로 붙일 수도 있음
public class AccountEventListener {
private final EmailService emailService;
@EventListener
public void handleAccountCreatedEvent(AccountCreatedEvent accountCreatedEvent) {
// 이메일 발송 로직
}
}
정리
스프링 만지면서 이벤트를 처음 접해봤는데 파도파도 유용한 신문물이 나오는 것 같다. 서비스 간 느슨한 의존성을 갖게하면서 사용자에게 관심있는 단어가 포함된 글이 생성될 때 알려준다거나, 댓글 알람, 용량이 큰 파일, 사진 등을 저장하는 등 프로세스 시간이 긴 서비스 로직 자체를 비동기로 처리하는 방법을 알게되서 매우 도움이 됐다.
참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1/dashboard
https://wildeveloperetrain.tistory.com/217