[Spring Boot] ApplicationEventPublisher를 통한 Event 처리

yesjm·2023년 4월 4일
1

ApplicationEventPublisher

이벤트를 발행하는 Publisher와 이를 감시하는 Observer(또는 Subscriber) 사이의 결합도를 낮추면서 이벤트를 Observer에게 전달하고 싶을 때 사용한다.

구현 방법

회원가입이 완료된 회원에게 가입 완료 메일을 보내는 예제이다

Publisher

@Service
@Transactional
class MemberService(
	private val memberRepository: MemberRepository,
	private val applicationEventPublisher: ApplicationEventPublisher
) {
	fun signUp(signUpRequestDto: SignUpRequestDto): SignUpResponseDto {
		val member = memberRepository.save(signUpRequestDto.toEntity())
		applicationEventPublisher.publishEvent(EmailSendEvent(member.email, this))
		return SignUpResponseDto(member.id)
	}
}
  • ApplicationEventPublisher의 publishEvent() 메소드를 호출해서 어플리케이션 이벤트를 발생시킬 수 있다.
  • 이메일 전송 이벤트를 발행함으로써 memberService에서 이메일 전송 관련 의존성이 필요없어졌다.
  • 이후 회원가입 쿠폰 발행, 알림톡 전송 등의 로직을 추가하더라고 memberService에 의존성을 추가할 필요 없이 이벤트 발행을 통해 해결할 수 있다.

Event

class EmailSendEvent(
    val email: String,
    source: Any
) : ApplicationEvent(source)

Listener

@Component
class EmailEventListener(
	private val sesService
) {
    @EventListener
    fun onEmailSendEventHandler(event: EmailSendEvent) {
        // 이메일 전송
		sesService.sendEmail(event.email)
    }
}
  • Listener는 스프링이 누구에게 이벤트를 전달할지 알아야하기 때문에 @Component를 사용해서 빈으로 등록한다.
  • @EventListener 어노테이션을 통해 Handler를 구현한다. 정해진 이벤트가 발생하면 이 어노테이션이 달린 메소드가 실행된다.

트랜잭션

@Service
@Transactional
class MemberService(
	private val memberRepository: MemberRepository,
	private val applicationEventPublisher: ApplicationEventPublisher
) {
	fun signUp(signUpRequestDto: SignUpRequestDto): SignUpResponseDto {
		val member = memberRepository.save(signUpRequestDto.toEntity())
		applicationEventPublisher.publishEvent(EmailSendEvent(member.email, this))
		// 회원가입 축하 쿠폰 발행 로직
		return SignUpResponseDto(member.id)
	}
}
@Component
class EmailEventListener(
	private val sesService
) {
    @TransactionalEventListener
    fun onEmailSendEventHandler(event: EmailSendEvent) {
        // 이메일 전송
		sesService.sendEmail(event.email)
    }
}
  • @EventListener 를 사용할 경우 이벤트를 발행하는 시점에 바로 리스닝을 진행하기 때문에 쿠폰 발행 로직에서 예외가 발생해 회원가입이 진행되지 않아도 이메일을 전송하는 문제가 생긴다.
  • @TransactionalEventListener으로 리스너를 등록하게 되면 해당 트랜잭션이 Commit된 이후에 리스너가 동작한다.
  • 쿠폰 발행 로직에서 예외가 발생하게 되면 트랜잭션이 Commit되지 않기 때문에 이메일 전송 리스너가 동작하지 않게 되어 트랜잭션 문제를 해결할 수 있다.

비동기 처리

  • 이벤트 핸들러는 기본적으로 동기적으로 실행된다.
  • 비동기적으로 이벤트를 수신하려면 이벤트 리스너에 @Async 어노테이션을 지정하고, 프로젝트 최상위 클래스 main 클래스에 @EnableAsync 어노테이션을 지정하면 된다.
  • @Async 추가를 통해 해당 메서드는 기존 스레드와 분리되고 자연스럽게 트랜잭션도 분리된다.

Listener

@Component
class EmailEventListener(
	private val sesService
) {
	@Async
    @EventListener
    fun onEmailSendEventHandler(event: EmailSendEvent) {
        // 이메일 전송
		sesService.sendEmail(event.email)
    }
}

main

@SpringBootApplication(scanBasePackages = ["jakarta.persistence"])
@EnableAsync
class ExampleApplication {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            runApplication<ExampleApplication>(*args)
        }
    }
}

.
.
.

참고
https://cheese10yun.github.io/event-transaction/
https://tecoble.techcourse.co.kr/post/2020-09-30-event-publish/

profile
yesjm의 개발블로그~

0개의 댓글