Spring
Spring Event[1] @EventListener 사용법
임요환
2024. 1. 21. 20:02
Spring의 @EventListener를 사용하는 이유?
- 컴포넌트 간의 결합도를 감소시켜 직접적인 의존성을 줄일 수 있음(느슨한 결합) -> 재사용성 높임
- PostService와 MailService가 있을 시 PostService안에 MailService에 대한 의존성을 가지고 있지 않고 Event를 발행함으로써 직접적인 의존도를 가지지 않음
- 새로운 기능이나 모듈을 추가할 때 적절한 이벤트를 생성하고 리스너를 등록함으로 기존 코드를 변경하지 않고 새로운 기능을 통합할 수 있는 확장성과 유연성을 제공함
- 클래스별 의존성을 줄이므로 테스트가 용이해짐
사용 방법
- 스프링 4.2 이전에는 ApplicationEvent를 상속받아야 하고 ApplicationListener 인터페이스를 구현해주어야 했지만 이후에는 @EventListener를 사용하면 간편하게 구현 가능함
- 스프링에서는 이벤트 발행 책임만 처리하는 ApplicationEventPublisher 인터페이스를 제공하므로 이벤트 발행은 해당 인터페이스를 주입받아 사용(publishEvent 메서드)하면 됨
EventListener 생성
@Component
class PostSaveEventListener {
private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)
@EventListener // 이벤트리스너
fun listen(event: PostSaveEvent) {
// send mail
logger.info("send mail")
}
}
Event 객체 생성
class PostSaveEvent( // 이벤트 객체
val post: Post
)
EventPublisher 사용
@Service
class PostService(
private val postRepository: PostRepository,
private val publisher: ApplicationEventPublisher // 이벤트 발행
) {
private val logger: Logger = LoggerFactory.getLogger(PostService::class.java)
@Transactional
fun savePost(): Post {
val savedPost = postRepository.save(Post("첫글", "첫글입니다", "잡담"))
publisher.publishEvent(PostSaveEvent(savedPost)) // publishEvent 메서드를 사용하여 이벤트를 발행함
return savedPost
}
}
내부 동작
- AppbstractApplicationContext.publishEvent를 호출
- getApplicationEventMulticaster()로 얻어온 ApplicationEventMulticaster에게 Event 처리를 위임
- SimpleApplicationEventMulticaster가 event를 등록된 리스너들에게 발행
주의 사항
동기적으로 동작함
@Component
class PostSaveEventListener {
private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)
@EventListener // 이벤트리스너
fun listen(event: PostSaveEvent) {
Thread.sleep(2000)
// send mail
logger.info("send mail")
}
}
- 위의 코드처럼 리스너에서 무거운 작업을 실행하게 되면 서비스가 응답을 하지 못하므로 시간이 오래걸리게 됨
- 이를 처리하기 위해 비동기로 처리해야 함
비동기 사용 두가지 방법
- @Async 애노테이션을 사용(@EnableAsync)
@Component
class PostSaveEventListener {
private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)
@EventListener // 이벤트리스너
@Async
fun listen(event: PostSaveEvent) {
Thread.sleep(2000)
// send mail
logger.info("send mail")
}
}
- Multicaster 등록
@Configuration
class AsynchronousSpringEventsConfig {
@Bean(name = ["applicationEventMulticaster"])
fun simpleApplicationEventMulticaster() : ApplicationEventMulticaster {
val eventMulticaster = SimpleApplicationEventMulticaster()
eventMulticaster.setTaskExecutor(SimpleAsyncTaskExecutor())
return eventMulticaster
}
}
기본적으로 Transaction이 전파됨
- 리스너에 @Transactional 애노테이션을 작성하지 않아도 Transaction이 기본적으로 전파됨
- 따라서 변경 감지(더티체킹)가 동작하고 더티체킹이 동작한다는 것은 PostSaveEvent의 엔티티 객체가 비영속상태가 아닌 영속상태로 존재함
@Component
class PostSaveEventListener(
private val postRepository: PostRepository
) {
private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)
@EventListener
fun listen(event: PostSaveEvent) {
// send mail
Thread.sleep(2000)
logger.info("Transaction name : {}", TransactionSynchronizationManager.getCurrentTransactionName())
logger.info("send mail")
event.post.apply {
this.content = "수정된 본문"
}
}
}
- 비동기로 선언할 시에는 다른 쓰레드에서 사용되므로 Transacition이 전파되지 않으므로 새로운 Transation을 선언한 후 영속상태의 엔티티를 가져와 변경감지를 사용하던가 save 메서드를 명시적으로 호출해야 함
@Component
class PostSaveEventListener(
private val postRepository: PostRepository
) {
private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)
@EventListener
@Transactional
@Async
fun listen(event: PostSaveEvent) {
// send mail
Thread.sleep(2000)
val foundPost = postRepository.findById(event.post.id!!).getOrNull() ?: throw RuntimeException()
logger.info("Transaction name : {}", TransactionSynchronizationManager.getCurrentTransactionName())
logger.info("send mail")
foundPost.apply {
this.content = "수정된 본문"
}
}
}
@Component
class PostSaveEventListener(
private val postRepository: PostRepository
) {
private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)
@EventListener
@Async
fun listen(event: PostSaveEvent) {
// send mail
logger.info("Transaction name : {}", TransactionSynchronizationManager.getCurrentTransactionName()) // com.yohwan.lab.post.PostService.savePost
logger.info("send mail")
event.post.apply {
this.content = "수정된 본문"
}
postRepository.save(event.post)
}
}
예외가 발생하면 서비스로 예외가 전파됨
- 서비스로 예외가 전파되면 에러가 발생하므로 서비스에서 try-catch 해야 함
- 비동기로 변경하게 되면 서비스로 예외가 전파되지는 않지만 따로 에러 핸들링을 해주어야 함
@Component
class PostSaveEventListener(
private val postRepository: PostRepository
) {
private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)
@EventListener
fun listen(event: PostSaveEvent) {
// send mail
Thread.sleep(2000)
logger.info("Transaction name : {}", TransactionSynchronizationManager.getCurrentTransactionName())
logger.info("send mail")
event.post.apply {
this.content = "수정된 본문"
}
throw RuntimeException()
}
}
@Service
class PostService(
private val postRepository: PostRepository,
private val publisher: ApplicationEventPublisher
) {
private val logger: Logger = LoggerFactory.getLogger(PostService::class.java)
@Transactional
fun savePost(): Post {
val savedPost = postRepository.save(Post("첫글", "첫글입니다", "잡담"))
try {
publisher.publishEvent(PostSaveEvent(savedPost))
} catch (e : RuntimeException) {
println("에러발생")
}
return savedPost
}
}
번외
- 만약 위와 같이 처리하게 되면 event.post의 content는 "수정된 본문"으로 변경이 될까 안될까?
- event.post의 content는 "수정된 본문"으로 변경된다
- 나는 에러가 발생하여 content가 "수정된 본문"으로 변경되지 않을 거라고 예상했는데 content는 변경되어 버렸다
- 이를 막기 위해 eventListener에 @Transational 애노테이션을 붙이면 해결이 될까?
- @Transactional 애노테이션을 붙이게 되면 오히려 rollback이 되어버린다 그러면 어떻게 해결하면 좋을까?
- 위의 코드를 식별자를 통해 조회하는 코드로 바꾼다고 본문이 변경 안 될까?
@Component
class PostSaveEventListener(
private val postRepository: PostRepository
) {
private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)
@EventListener
fun listen(event: PostSaveEvent) {
// send mail
Thread.sleep(2000)
val foundPost = postRepository.findById(event.post.id!!).getOrNull() ?: throw RuntimeException()
logger.info("Transaction name : {}", TransactionSynchronizationManager.getCurrentTransactionName())
logger.info("send mail")
foundPost.apply {
this.content = "수정된 본문"
}
throw RuntimeException()
}
}
- 이 코드에서 findById 하는 객체는 영속성 컨텍스트에 존재하는 savedPost와 같은 엔티티가 조회되므로 에러가 발생해도 본문은 변경된다.
- 이를 해결하기 위해 @Transactional(propagation = Propagation.REQUIRES_NEW)를 통해 새로운 트랜잭션을 생성하고 Event에 엔티티 대신 식별자를 넘겨 조회하여 수정하면 해결할 수 있다
@Component
class PostSaveEventListener(
private val postRepository: PostRepository
) {
private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)
@EventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun listen(event: PostSaveEvent) {
// send mail
Thread.sleep(2000)
val foundPost = postRepository.findById(event.post.id!!).getOrNull() ?: throw RuntimeException()
logger.info("Transaction name : {}", TransactionSynchronizationManager.getCurrentTransactionName())
logger.info("send mail")
foundPost.apply {
this.content = "수정된 본문"
}
throw RuntimeException()
}
}
- 이렇게 하게 되면 예외 발생 시 content가 "수정된 본문"으로 변경되지 않고 내가 원했던 결과를 얻을 수 있다.
요약
- spring event를 사용하게 되면 컴포넌트 간의 의존성을 줄여 재사용성을 높일 수 있음
- event는 동기적으로 동작하지만 비동기로 사용하는 것이 효율적임
- event 객체를 만들 때 엔티티 자체를 넘기는 것보다 식별자 값만 넘기는 게 활용성 높음(비동기 시 Transaction 활용)
- service에서 event를 try-catch 하지 않도록 비동기를 사용하는 것이 좋음
- EventListener보다 TransactionEventListener가 더 활용성이 높으므로 이걸 사용하는 걸 추천함