Spring events는 하나의 spring application 내에서 event를 통해 bean간에 데이터를 주고 받는 방식입니다.
오늘은 왜 쓰는지 그리고 어떻게 사용하는지를 간단히 알아보겠습니다.
개발을 하다보면 매우 많은 빈도로 "결합도"와 "응집도"에 대한 이야기를 듣곤합니다.
결합도와 응집도가 무엇일까요?
먼저 결합도는 보통 모듈 간의 상호 의존 정도를 나타냅니다. 두 모듈 간에 높은 결합도가 있다는 것은 하나의 모듈이 다른 모듈에 대해 많은 정보를 알거나 의존하고 있다는 것을 의미하고, 결국 결합도가 높으면 하나의 변경이 서로에게 영향을 주기 쉽다는 것을 의미합니다.
응집도는 모듈 내부의 요소들이 얼마나 밀접하게 관련되어 있는지를 의미합니다. 하나의 모듈의 응집도가 높다는 것은 내부의 요소들이 비슷한 목적을 가지고 밀접하게 협력함을 의미하며, 모듈이 한 가지 기능 또는 목적을 가진다는 것을 의미합니다.
그래서 결국 "결합도 낮을수록, 응집도는 높을수록 좋다"라고 생각하면 됩니다.
그런데 갑자기 왜 결합도와 응집도 얘기를 할까요?
Spring event를 사용하면 두 모듈간의 의존성을 분리할 수 있기 때문입니다. 두 모듈이 직접적으로 의존하고 있는 것이 아니기 때문에, 이벤트를 발행하는 곳에서는 실제로 그 이벤트를 어떻게 사용하는지를 몰라도 되고, 이벤트를 수신하는 곳의 로직을 수정할 때는 이벤트를 발행하는 곳의 로직을 고민하지 않아도 됩니다.
그렇기에 Spring Event 는 그 관점에서, 모듈의 결합도는 낮춰주고 응집도를 높여줄 수 있는 방법입니다.
또한, 제 생각으로는 applicationPublisher를 추후 kafka 등으로 분리하고 event listener 등의 동작도 아예 다른 서버등으로 분리하기에 아주 편한 방식인 것 같습니다.
이제 사용하는 방법을 알아보겠습니다.
Spring boot 1.3 혹은 Spring 4.2 버전 이하에서는 ApplicationEvent 클래스를 상속하고, ApplicationListener 인터페이스를 구현해야하는 다소 번거로운 방식으로 지원했었습니다.
하지만 그 이후부터는 ApplicationEvent 상속없이 이벤트 발행이 가능해졌으며, 이벤트를 소비하는 쪽에서는 @EventListener
어노테이션만 함수 위에 다는 방식으로 구현이 가능해졌습니다.
data class Event(
val field: String
)
@Service
class SomeService(
private val applicationPublisher: ApplicationPublisher
) {
fun test() {
applicationPublisher.publishEvent(Event("테스트 이벤트"))
}
}
@Component
class SomeEventListener {
@EventListener
fun handleEvent(event: Event) {
log.info("${event.field} 이벤트 수신 완료")
}
}
특정한 상황에서 Event는 트랜잭션과 함께 사용되어야하는 경우도 있습니다.
ApplicationListener는 트랜잭션과 이러한 경우에서, TransactionalEventListener라는 방식으로 트랜잭션과 함께 사용할 수 있는 방식을 지원합니다.
사용방식은 간단합니다.
data class Event(
val field: String
)
@Service
class SomeService(
private val applicationPublisher: ApplicationPublisher
) {
@Transactional
fun test() {
applicationPublisher.publishEvent(Event("테스트 이벤트"))
}
}
@Component
class SomeTransactionalEventListener {
@TransactionalEventListener
fun handleEvent(event: Event) {
log.info("${event.field} 이벤트 수신 완료")
}
}
TransactionalEventListener에는 해당 리스너에 이벤트를 보낸 함수의 트랜잭션 상태에 따라 동작시킬 수 있는 phase 옵션을 제공합니다.
당연하게도 AFTER_COMMIT, ROLLBACK, AFTER_COMPLETION 과 같이 이벤트를 전달한 곳에서 transaction이 종료되었다면, event listener는 해당 트랜잭션을 재사용하지 못하기 때문에 transactional 이 선언되어있어도 동작하지 않습니다.
그렇기에 이런 경우 @Transactional(propagation = Propagation.REQUIRES_NEW)
을 설정해줘야합니다.
BEFORE_COMMIT으로 선언한 경우, 이전 트랜잭션과 합류하여 동작하기 때문에 따로 설정할 필요는 없습니다.
@Async
어노테이션을 달고 @EnableAsync
등의 configuration을 따로 설정해줘야 비동기로 동작하게 됩니다.