스프링 이벤트

Woody·2024년 8월 26일

TIL

목록 보기
8/19

구현

이벤트 구조는 3가지 구성 요소(이벤트 , 발행자 , 구독자)가 필요하고, 이는 스프링에서 쉽게 구현할 수 있다.

1. 이벤트

발행자와 구독자 간에 사용할 이벤트를 아래와 같이 정의할 수 있다. 여기서는 이메일 전송을 위한 이벤트를 예시로 들겠다.

public record SendEmailEvent(String email) {
}

2. 구독자

@EventListener 어노테이션을 통해 이벤트 발생 시, 실행할 작업을 쉽게 정의할 수 있다.

아래 예시에서는 메일 전송 이벤트가 발행될 때, listen 함수 내부에 정의된 작업들이 실행된다.

@Component
@RequiredArgsConstructor
public class SendEmailEventListener {
	private final EmailService emailService;

    @EventListener
    public void listen(SendEmailEvent sendEmailEvent) {
		emailService.send(sendEmailEvent.email());
    }
}

3. 발행자

스프링의 ApplicationEventPublisher를 사용하여 이벤트를 발행할 수 있다. 아래와 같이 ApplicationEventPublisher를 주입을 받고, publishEvent 함수를 통해서 이벤트를 발행한다.

이벤트 발행 시, 해당 이벤트를 구독 중인 리스너들이 반응하여 정의된 작업을 수행한다.

@Service
@RequiredArgsConstructor
public class CreateUserService {
	private final UserRepository userRepository;
    private final ApplicationEventPublisher publisher;

	@Transactional
    public void save() {
	    userRepository.save(new User("KWY"));
        publisher.publishEvent(new SendEmailEvent("KWY@gmail.com"));
    }
}

이렇게 발행자와 구독자 간의 이벤트 기반 구조를 스프링에서 쉽게 구현할 수 있다.

또한, 스프링 이벤트는 멀티캐스팅 방식이기 때문에, 같은 이벤트를 구독하는 리스너가 여러 개 있을 수 있으며, 이벤트가 발행되면 해당 이벤트를 구독 중인 모든 리스너가 동작한다.

아쉬운 점 : 동기

도메인 간 의존성을 덜 수 있다는 장점 외에도 API 응답 시간 단축을 기대했다. 하지만 스프링 이벤트 방식은 동기 방식으로 동작하기 때문에 리스너들의 작업이 끝난 후에 API가 종료된다. 이를 해결하기 위해, @Async 어노테이션을 통해 이벤트를 비동기 방식으로 동작하도록 만들 수 있다.

Application에는 @EnableAsync 어노테이션을, 리스너 함수에는 @Async 어노테이션을 정의하면 쉽게 이벤트를 비동기 방식으로 동작하게 만들 수 있다.

@EnableAsync
public class ServerApplication {
    public static void main(String[] args) { SpringApplication.run(ServerApplication.class, args); }
}
@Component
@RequiredArgsConstructor
public class SendEmailEventListener {
	private final EmailService emailService;

    @EventListener
	@Async
    public void listen(SendEmailEvent sendEmailEvent) {
	    emailService.send(sendEmailEvent.email());
    }
}

하지만 @Async 어노테이션은 호출될 때마다 스레드를 생성하기 때문에 이벤트가 발생할 때마다 스레드를 생성, 제거하는 비용이 든다. 이를 해결하기 위해 스레드 풀을 미리 정의하는 방식을 사용한다.

@Configuration
@EnableAsync
public class SpringAsyncConfig {
	/*
		평소 스레드 최대 3개의 스레드 동작
		3개 이상의 스레드 요청이 동시에 들어올 때, 크기가 50인 큐에서 대기
		대기 큐가 가득 찼을 때, 최대 10개의 스레드 생성
    */
    @Bean
    public Executor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(3);
        taskExecutor.setQueueCapacity(50);
        taskExecutor.setMaxPoolSize(10);
        return taskExecutor;
    }
}

위와 같이 Application이 아닌 Config에 @EnableAsync 어노테이션을 붙이고, 스레드 풀을 정의한다. 이후 이벤트가 발생할 때마다 매번 스레드를 생성하지 않고, Config에 정의했던 스레드 풀에서 스레드를 가져다 쓰고 반납한다.

이벤트와 트랜잭션

각 이벤트마다 트랜잭션에 포함이 될 수도, 안 될 수도 있기 때문에, 이를 위해 스프링은 @TransactionEventListener를 제공하고 있다.

@EventListener 대신 사용하면 되며, 옵션은 다음과 같다.

  • BEFOR_COMMIT : : 이벤트 발행 주체가 커밋이 되기 전에 이벤트 실행

  • AFTER_COMPLETION : : 이벤트 발행 주체가 커밋 또는 롤백이 된 후에 이벤트 실행

  • AFTER_COMMIT (default) : 이벤트 발행 주체가 커밋이 되면 이벤트 실행

  • AFTER_ROLLBACK : 이벤트 발행 주체가 롤백이 되면 이벤트 실행

주의할 점 1

AFTER_ 관련 옵션은 이벤트가 실행될 때, 트랜잭션이 이미 커밋 또는 롤백되어 종료된 상태이다. 따라서 @TransactionEventListener 내부에서 데이터를 저장 또는 수정하는 작업은 DB에 반영되지 않는다.

이를 해결하기 위해서 이벤트 함수에 @Transactional(propagation = Propagation.REQUIRES_NEW) 어노테이션을 정의해서 이벤트가 발생할 때마다 트랜잭션을 새롭게 만들어 줘야 한다.

주의할 점 2

@Transactional(propagation = Propagation.REQUIRES_NEW) 어노테이션이 붙은 이벤트가 N번 발생한다면, N개의 트랜잭션이 생성되어 DB 커넥션을 점유한다.

해당 이벤트가 동기 방식이라면 스레드가 종료(API가 종료)될 때까지 DB 커넥션을 계속 점유한다. 이를 해결하기 위해서 비동기 방식으로 이벤트를 실행함으로써, 각 이벤트가 종료될 때마다 DB 커넥션이 반납되도록 할 수 있다.

참고

[Spring] 스프링에서 이벤트의 발행과 구독 방법과 주의사항, 이벤트 사용의 장/단점과 사용 예시

스프링 이벤트 적용기

0개의 댓글