관리 메뉴

개발그래머

Spring Event[1] @EventListener 사용법 본문

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")
    }
}
  • 위의 코드처럼 리스너에서 무거운 작업을 실행하게 되면 서비스가 응답을 하지 못하므로 시간이 오래걸리게 됨
  • 이를 처리하기 위해 비동기로 처리해야 함

비동기 사용 두가지 방법

  1. @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")
    }
}
  1. 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가 더 활용성이 높으므로 이걸 사용하는 걸 추천함