Spring Events ( ApplicationEventListener, TransactionEventListener )

kshired·2024년 1월 26일
0

Spring events는 하나의 spring application 내에서 event를 통해 bean간에 데이터를 주고 받는 방식입니다.

오늘은 왜 쓰는지 그리고 어떻게 사용하는지를 간단히 알아보겠습니다.

왜 사용할까?

개발을 하다보면 매우 많은 빈도로 "결합도"와 "응집도"에 대한 이야기를 듣곤합니다.

결합도와 응집도가 무엇일까요?

먼저 결합도는 보통 모듈 간의 상호 의존 정도를 나타냅니다. 두 모듈 간에 높은 결합도가 있다는 것은 하나의 모듈이 다른 모듈에 대해 많은 정보를 알거나 의존하고 있다는 것을 의미하고, 결국 결합도가 높으면 하나의 변경이 서로에게 영향을 주기 쉽다는 것을 의미합니다.

응집도는 모듈 내부의 요소들이 얼마나 밀접하게 관련되어 있는지를 의미합니다. 하나의 모듈의 응집도가 높다는 것은 내부의 요소들이 비슷한 목적을 가지고 밀접하게 협력함을 의미하며, 모듈이 한 가지 기능 또는 목적을 가진다는 것을 의미합니다.

그래서 결국 "결합도 낮을수록, 응집도는 높을수록 좋다"라고 생각하면 됩니다.

그런데 갑자기 왜 결합도와 응집도 얘기를 할까요?

Spring event를 사용하면 두 모듈간의 의존성을 분리할 수 있기 때문입니다. 두 모듈이 직접적으로 의존하고 있는 것이 아니기 때문에, 이벤트를 발행하는 곳에서는 실제로 그 이벤트를 어떻게 사용하는지를 몰라도 되고, 이벤트를 수신하는 곳의 로직을 수정할 때는 이벤트를 발행하는 곳의 로직을 고민하지 않아도 됩니다.

그렇기에 Spring Event 는 그 관점에서, 모듈의 결합도는 낮춰주고 응집도를 높여줄 수 있는 방법입니다.

또한, 제 생각으로는 applicationPublisher를 추후 kafka 등으로 분리하고 event listener 등의 동작도 아예 다른 서버등으로 분리하기에 아주 편한 방식인 것 같습니다.

Application Event 사용하기

이제 사용하는 방법을 알아보겠습니다.

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} 이벤트 수신 완료")
    }
}

TransactionEventListener 사용하기

특정한 상황에서 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 옵션을 제공합니다.

  • TransactionPhase.AFTER_COMMIT : 기본 값으로, 트랜잭션이 commit 되었을 때 listener가 동작합니다.
  • TransactionPhase.ROLLBACK : 트랜잭션이 rollback 되었을 때 listener가 동작합니다.
  • TransactionPhase.AFTER_COMPLETION : 트랜잭션이 commit 혹은 rollback 되었을 때 listener가 동작합니다.
  • TransactionPhase.BEFORE_COMMIT : 기본 값으로, 트랜잭션이 commit 되기전 listener가 동작합니다.

TransactionalEventListener 에서 transaction이 필요하다면?

당연하게도 AFTER_COMMIT, ROLLBACK, AFTER_COMPLETION 과 같이 이벤트를 전달한 곳에서 transaction이 종료되었다면, event listener는 해당 트랜잭션을 재사용하지 못하기 때문에 transactional 이 선언되어있어도 동작하지 않습니다.

그렇기에 이런 경우 @Transactional(propagation = Propagation.REQUIRES_NEW) 을 설정해줘야합니다.

BEFORE_COMMIT으로 선언한 경우, 이전 트랜잭션과 합류하여 동작하기 때문에 따로 설정할 필요는 없습니다.

주의점

  • Spring Event 는 기본적으로 멀티캐스팅 방식으로 동작합니다. 하나의 이벤트에 대한 여러 리스너가 등록되어있으면, 그 이벤트를 모든 리스너가 수신하여 동작하게 되니 이 부분을 주의해야합니다.
  • 또한, 기본적으로 동기적으로 동작하기 때문에 비동기 동작이 필요하다면 @Async 어노테이션을 달고 @EnableAsync 등의 configuration을 따로 설정해줘야 비동기로 동작하게 됩니다.
profile
글 쓰는 개발자

0개의 댓글