컴포넌트 간의 결합도를 감소시켜 직접적인 의존성을 줄일 수 있음(느슨한 결합) -> 재사용성 높임
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가 더 활용성이 높으므로 이걸 사용하는 걸 추천함