회사에서 로그나 공지 등록시 자동적으로 알림을 생성하는 서비스를 구현해야 했습니다.
이를 위해서는 DI로 기존의 공지 등록 로직에 알림 생성 로직을 주입하지 않고 이벤트를 활용하여 구현하기로 했습니다.
처음에는 단순히 공지 서비스 레이어에 알림 서비스를 DI 를 적용하려 했습니다.
NoticeService.java
@RequiredArgsConstructor
@Transactional
@Service
public class NoticeService {
private final NoticeRepository noticeRepository;
private final NotificationService notificationService; // DI
public void createNotice(String message, String registerName) {
Notice notice = Notice.builder()
.message(message)
.registerName(registerName)
.build();
Notice savedNotice = noticeRepository.save(notice);
notificationService.createNotification(savedNotice); // 알림 등록
}
}
하지만 위와 같은 방식은 문제점이 있습니다.
위의 코드를 보시다시피 공지를 등록하는 로직 바로 다음에 알림 생성을 하는 로직이 같이 섞여 있습니다.이런 방법은 서로 강한 결합으로 유지보수가 어려워지고, 나중에 NotificationService
에 수정이 일어나면 NoticeService
까지 수정해야 하는 상황이 발생하게 될 수 있습니다.
스프링 이벤트를 활용하면 공지 등록 로직과 알림 생성 로직간의 결합도를 끊어내여 관심사를 분리할 수 있습니다.
Spring Event 활용 전
Spring Event 활용 후
기존의 의존성 주입을 받는 방식과는 달리, Event Class(NoticeService) 에서 EventPublisher로 인해 이벤트가 발생하면, ApplicationContext가 이를 감지하고 ApplicationEventListener가 붙어있는 메서드에 이벤트를 전달하는 방식으로 동작하여 관심사를 분리할 수 있습니다.
아래는 Spring Docs에서 Event의 동작 방식에 대한 설명입니다.
Event handling in the ApplicationContext is provided through the ApplicationEvent class and the ApplicationListener interface. If a bean that implements the ApplicationListener interface is deployed into the context, every time an ApplicationEvent gets published to the ApplicationContext, that bean is notified. Essentially, this is the standard Observer design pattern.
ApplicationContext
에 구현된 ApplicationListener
인터페이스를 가진 빈이 배치되면 ApplicationContext
에 ApplicationEvent
가 발행될 때 마다 옵저버 패턴을 통해 해당 Bean에 전달되는 방식으로 진행되는 것을 알 수 있습니다.
@FunctionalInterface
public interface ApplicationEventPublisher {
void publishEvent(Object event);
}
ApplicationContext에 이벤트를 발행해주는 인터페이스입니다. 이벤트에 필요한 데이터를 저장할 수 있습니다.
ApplicationContext
에 이벤트가 발생하면 @EventListener
가 붙은 메서드를 찾아 실행합니다. 이때 .publishEvent(Object event)
로 발행한 event
를 파라미터로 받는 메서드를 모두 실행하기 때문에 이벤트를 받을 메서드의 파라미터의 중복에 주의해야 합니다.
NoticeService
@RequiredArgsConstructor
@Transactional
@Service
public class NoticeService {
private final NoticeRepository noticeRepository;
private final ApplicationEventPublisher applicationEventPublisher;
public Long createNotice(String message, String registerName) {
Notice notice = Notice.builder()
.message(message)
.registerName(registerName)
.build();
Notice savedNotice = noticeRepository.save(notice);
applicationEventPublisher.publishEvent(savedNotice); //이벤트 발행
return savedNotice.getId();
}
}
CustomEventListener
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
//이벤트 발행 시 실행
@RequiredArgsConstructor
@Component
public class CustomEventListener {
private final NotificationService notificationService;
@EventListener
public void handler(Notice notice) {
notificationService.createNotification(notice);
}
}
그러나 위와 같이 고친 결과, 또 다른 문제가 발생하였습니다.
알림 서비스에 문제가 발생하여 롤백이 이뤄지면 공지 등록까지 전부 롤백이 되는 문제가 발생했습니다.
원인은 @EventListner
가 동기 방식으로 동작하기 때문이었습니다.
이로 인해 트랜잭션이 하나의 범위에 묶이기 때문에, 이벤트를 발행하는 곳에서부터 트랜잭션이 시작되어 @EventListener
가 달린 곳까지 트랜잭션을 공유하기 때문에 알림 생성이 롤백이 되면 공지 등록도 롤백이 된 것이었습니다.
이 문제는
이 두가지 방법 중 하나로 해결할 수 있었습니다.
Transaction 문제를 해결하기 위해서는 EventListener
대신 TransactionalEventListener
를 사용하면 됩니다.
@RequiredArgsConstructor
@Component
public class CustomEventListener {
private final NotificationService notificationService;
// 이벤트 발행된 곳의 트랜잭션 완료 후 실행
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handler(Notice notice) {
notificationService.createNotification(notice);
}
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
에서
phase = TransactionPhase.AFTER_COMMIT
는 Transaction이 성공적으로 commit된 후 Listener 로직을 수행하라는 뜻입니다.
phase에는 4가지 옵션을 지정할 수 있습니다.
@TransactionalEventListener
을 통해 트랜잭션을 처리할 수 있으나 AFTER_COMMIT를 사용할 때 주의사항이 있습니다.
AFTER_COMMIT
사용 시 이전의 이벤트를 publish 하는 코드에서 트랜잭션이 이미 커밋되었기 때문에
@EventListener
가 달린 메서드에는 트랜잭션이 적용되지 않습니다.
그래서 @Transactional
적용시 PROPAGATION_REQUIRE_NEW
를 적용시켜주어 새로운 트랜잭션을 열어줘야 했습니다.
@RequiredArgsConstructor
@Component
public class CustomEventListener {
private final NotificationService notificationService;
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION) // 이벤트 발행된 곳의 트랜잭션 완료 후 실행
public void handler(Notice notice) {
notificationService.createNotification(notice);
}
}
@Async
어노테이션을 추가하여 비동기로 처리하는 방법이 있습니다. 이로 인해 같은 트랜잭션으로 묶이지 않고 서로 다른 스레드에서 동작하게 할 수 있습니다.
@RequiredArgsConstructor
@Component
public class CustomEventListener {
private final NotificationService notificationService;
@Async
@TransactionalEventListener
public void handler(Notice notice) {
notificationService.createNotification(notice);
}
}
이때, @Async를 활용하기 위해 Application 클래스에 @EnableAsync를 추가해주어야 합니다.
@EnableAsync
@SpringBootApplication
public class EventhandlerApplication {
public static void main(String[] args) {
SpringApplication.run(EventhandlerApplication.class, args);
}
}
만약 추가적으로 @Async에 관한 스레드 관리를 하고 싶다면 아래의 코드처럼 config 설정을 해줄 수 있습니다.
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(3); // 기본 스레드 수
threadPoolTaskExecutor.setMaxPoolSize(30); // 최대 스레드 수
threadPoolTaskExecutor.setQueueCapacity(10); // Queue 수
threadPoolTaskExecutor.setThreadNamePrefix("Executor-");
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
/*
* 처음엔 core 사이즈 만큼 동작하다
* core 사이즈 만큼 스레드에서 task를 처리할 수 없을 경우 queue에서 대기한다.
* queue가 꽉 차게 되면 그때 최대 max 사이즈 만큼 스레드를 생성해서 처리한다.
* */
}
}