스프링 어플리케이션 이벤트

June·2023년 6월 11일
0

실무 문제

목록 보기
8/10

배경

가위바위보 이벤트를 하면서 푸시를 보낼일이 많이 생겼다. 예를 들어 가위바위보에 승리하고 나면 1승을 추가로 하면 또 다른 무언가를 얻을 수 있다고 푸시를 보낸다. 처음에는 이런 것들이 그렇게 많지 않아서 코드에서 직접적으로 푸시를 보내는 코드를 추가했었다.

fun 게임_보상_받는_메서드() {
	...
    // 리워드를 받는 코드들
    ...
    if (winner.hasWin()) {
    	sendPush()
    }
    ...

하지만 이번에 보내는 푸시 종류가 6개가 되다보니 코드에서 점점 핵심 로직과 조금은 동떨어진 푸시 보내는 코드들이 섞이기 시작햇다.

fun 게임_보상_받는_메서드() {
	...
    // 리워드를 받는 코드들
    ...
    if (winner.hasWin()) {
    	sendPush()
    }
    ...
    if (legend.hasRecord()) {
		sendPush2()
    }
}

또 가위바위보 승리를 저장하는 핵심 기능이 실패하면 푸시 발송이라는 부가적인 기능도 같이 실패하게 만들고 싶었다. 그래서 어플리케이션 이벤트를 이용해서 도메인 로직에 조금더 집중 해보기로 했다.

어플리케이션 이벤트

RewardFacade

class RewardFacade(
	...
    private val rewardService: RewardService,
    private val applicationEventPublisher: ApplicationEventPublisher,
    ...
){
	
    fun winReward(): RewardResponse {
    	...
        // 리워드 받는 코드
        rewardService.save(
        	...
        ).also {
        	applicationEventPublisher.publishEvent(
            	RewardEvent(
                	userId = userId,
                    ...
                )
            )
        }
    }
}

RewardEventListener

class RewardEventListener(
	private val cacheEvictor: CacheEvictor,
    private val messageSender: MessageSender,
    ...
) {
	
    @Async
    @EventListener
    fun listen(event: RewardEvent) {
    	messageSender.sendPush(evnet)
        cacheEvictor.evictDeliveryRewards()
    }
    
    ...
}

예전에는 winReward() 메서드에서 직접 리워드를 받았을 경우 후속 조치들을 해줬었다. (푸시1번 발송, 푸시 2번 발송...). 요구 사항이 추가된다면 이 메서드에서 계속 다른 서비스나 메서드들을 호출해줘야 한다. 실제로 리워드를 받아서 리워드에 대한 캐시도 evict 시켜줘야 하는데 추가하기도 했다.

그렇게 되면 부가적인 로직들이 점점 많이 들어가게 되고 서비스들간의 결합이 강해진다. 새로운 예시 코드를 보면 그냥 이벤트를 publish하고 있다. 그러면 이벤트를 구독하고 있는 곳에서 부가적인 로직들을 처리할 수 있다.

@TransactionalEventListener

class RewardService(
	private val rewardRepository: RewardRepository,
    private val applicationEventListener: ApplicationEventPublisher,
){
	
    @Transactional
    fun saveReward() {
    	rewardRepository.save(
        	...
        ).also {
        	applicationEventPublisher.publishEvent(
            	RewardEvent(userId = userId)
            )
        }
    }
}

어떨때는 DB와 정합성을 맞추는 것이 중요할 때도 있다. @Transactional을 이용하여 이벤트를 통제할 수도 있다.

class RewardEventListener(
	private val cacheEvictor: CacheEvictor,
    private val messageSender: MessageSender,
    ...
) {
	
    @Async
    @TransactionalEventListener
    fun listen(event: RewardEvent) {
    	messageSender.sendPush(evnet)
    }
    
    ...
}

TransactionalEventListener를 사용하면 트랜잭션의 커밋 시점에 이벤트를 처리하게 할 수도 있다.

TransactionalEventListener에는 옵션들을 줄 수 있다.

  • TransactionPhase.AFTER_COMMIT: 커밋 됐을 때 이벤트 실행
  • TransactionPhase.AFTER_ROLLBACK: 롤백 됐을 때 이벤트 실행
  • TransactionPhase.AFTER_COMPLETION: 커밋 또는 롤백됐을 때
  • TransactionPhase.BEFORE_COMMIT: 커밋되기 전에

어플리케이션 이벤트에 대한 오해

사실 예전 우테코에서 속닥속닥 프로젝트를 진행할 때 어플리케이션 이벤트를 도입하자고 제안하기 위해, 혼자 개발을 해본적이 있다. 회원 가입을 할때 인증 버튼을 누르면 서버에서 난수 번호를 생성해서 이메일로 보내도록 했었는데, 이 부분에서 도입을 했었다.

당시 기능이 정상적으로 돌아갔고 테스트 코드까지 다 작성하였지만 마음에 들지 않아서 결국 제안을 하지 않았던 기억이 있다. 그때 마음에 들지 않았던 부분은 어플리케이션 이벤트를 쓰면 불필요한 강한 결합이 느슨해지게 할 수 있다해서 써봤는데 그런 느낌이 전혀 들지 않았던 것이다. 당시에는 @Async를 이용하는 것과 이벤트를 발행하는 것을 하나의 개념으로 묶어서 생각해서 그랬던 것 같다.

지금와서 생각해보면 당시의 문제점은 코드적으로는 느슨한 결합인 것처럼 보이게는 만들었으나 논리적으로는 강한 결합을 하고 있었기 때문이었다. '이메일 발송 서비스야 신규 회원이 이메일 인증 시도하니 이메일 인증 시도 이벤트를 보낼게. 너가 이메일 보내!' 라고 수신자에게 어떤일을 할지 명령형으로 알려주는 구조였다. 이렇게 해서는 느슨한 결합이 나올 수 없다.

회원시스템 이벤트기반 아키텍처 구축하기에서 4:05초 부분에도 나오지만 이벤트를 사용하는 것의 핵심은 두 서비스나 시스템의 강한 결합을 끊어내고 느슨하게 만들기 위해서다. 그러기 위해서는 이벤트를 발생 시키는 쪽은 마치 브로드캐스트 방식처럼 '회원 가입 시도 이벤트가 발생했다!'라 하고 소리치고 거기서 끝내야 한다.

그럼 그 이벤트가 필요한 곳에서는 받아서 사용하는 구조로 이뤄진다.

참고

https://www.baeldung.com/spring-events
https://chamggae.tistory.com/204
https://leeheefull.tistory.com/15
https://newwisdom.tistory.com/m/127
https://sukyology.tistory.com/18

0개의 댓글