[Spring Boot] EventHandler

개발하는 구황작물·2023년 12월 3일
0

회사 프로젝트

목록 보기
1/8
post-thumbnail

회사에서 로그나 공지 등록시 자동적으로 알림을 생성하는 서비스를 구현해야 했다.

이를 위해서는 DI로 기존의 공지 등록 로직에 알림 생성 로직을 주입하지 않고 이벤트를 활용하여 구현하기로 하였다.

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

스프링 이벤트를 활용하면 공지 등록 로직과 알림 생성 로직간의 결합도를 끊어내여 관심사를 분리할 수 있다.

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의 이벤트 처리는 ApplicationEvent클래스와 ApplicationListener인터페이스를 통해 제공된다.
ApplicationContext에 구현된 ApplicationListener인터페이스를 가진 빈이 배치되면 ApplicationContextApplicationEvent가 발행될 때 마다 해당 Bean에 전달되는 방식으로 진행된다고 한다.(Observer Pattern 방식)

...라고 한다.

원래는 ApplicationListener인터페이스를 구현해야 했으나 스프링 4.2 이상부터는 어노테이션 방식으로도 구현이 가능하다고 한다.

이 글에서는 어노테이션 방식에 대해 설명하도록 하겠다.

ApplicationEventPublisher

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

ApplicationContext에 이벤트를 발행해주는 인터페이스이다. 이벤트에 필요한 데이터를 저장할 수 있다.

@EventListener

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);
    }
}

문제점

그러나 위의 로직에도 문제점이 있다.

  1. 공지 등록과 알림 생성이 같은 트랜잭션에 묶여 있다.
    만약 공지 등록은 성공적으로 일어났으나 알림 생성에 문제가 생긴다면 같은 트랜잭션인 경우에는 공지 등록까지 전부 롤백을 시켜야 하는 경우가 발생한다. 이를 위해 공지 등록 트랜잭션이 성공적으로 끝난 이후 알림 생성이 이루어져야 한다.

  2. 동기적으로 움직인다.
    공지가 생성된 이후로는 알림 생성과 동기적으로 일어나야 할 이유가 없다. 동기적으로 움직이면 오히려 공지 생성의 시간만 길어질 뿐이다.

Transaction 문제 해결

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가지 옵션을 지정할 수 있다.

  • BEFORE_COMMIT
  • AFTER_COMMIT
  • AFTER_ROLLBACK
  • AFTER_COMPLETED : AFTER_ROLLBACK + AFTER_COMMIT

@TransactionalEventListener을 통해 트랜잭션을 처리할 수 있으나 AFTER_COMMIT를 사용할 때 주의사항이 있다.

AFTER_COMMIT 사용 시 이전의 이벤트를 publish 하는 코드에서 트랜잭션이 이미 커밋되었기 때문에 AFTER_COMMIT 이후 새로운 트랜잭션 수행 시 트랜잭션을 커밋하지 않는다고 한다.

그래서 @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(phase = TransactionPhase.AFTER_COMPLETION) // 이벤트 발행된 곳의 트랜잭션 완료 후 실행
    public void handler(Notice notice) {
        notificationService.createNotification(notice);
    }
}

(@Async를 사용하기 위해서는 별도의 설정을 해주어야 하는데 이는 아래에 설명하도록 하겠다)

@TransactionalEventListener의 phase를 BEFORE_COMMIT로 설정하여 커밋이 되기 전에 이벤트 리스너의 로직을 실행 시키면 된다. (그러나 이벤트 리스너 로직에 예외가 발생하면 핵심 로직 트랜잭션에 영향을 끼치게 되므로 주의해서 사용해야 한다.)

@Async 설정

이벤트 리스너를 비동기적으로 설정하기 위해서는 @Async를 추가해주면 된다.

@RequiredArgsConstructor
@Component
public class CustomEventListener {
    private final NotificationService notificationService;

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION) // 이벤트 발행된 곳의 트랜잭션 완료 후 실행
    public void handler(Notice notice) {
        notificationService.createNotification(notice);
    }
}

그리고 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 사이즈 만큼 스레드를 생성해서 처리한다.
         * */
    }
}

config클래스를 설정해주면 런타임시 config 클래스의 threadPoolTaskExecutor bean 정보를 읽어들인다.

위의 config 클래스에 @EnableAsync를 설정해주었으므로 Application 클래스의 @EnableAsync를 제거해주면 된다.

profile
어쩌다보니 개발하게 된 구황작물

0개의 댓글