본문 바로가기
개발 관련/spring

eventPublisher 와 eventListenr 사용

by lazysnack 2022. 7. 14.

서론

최근 마이크로미터를 사용하여 해당 서비스의 접속 카운트를 표시할까 해봤다. (오로지 흥미 위주로..)

처음에는 MeterRegistry 와 Counter 를 직접 사용하여 해당 메소드에 추가해주는 식으로 구현했었다.

구현하고 보니 정작 이거 counter 부분에서 에러가 나면 어떻게 되지? 중요 비즈니스 로직과는 무관하게 진행되야 하는건데 라는 생각이 들어서 다른 방법을 알아보던 차에 다른 방법을 생각해봤다.

처음에는 인터셉터의 afterCompletion 을 사용해봤으나, lateinit 을 이용하고 해도 계속 초기화 문제가 발생해서 이리저리 해보다가 결국은 이렇다할 해결책을 못 찾고(코틀린과 스프링에 대한 지식이 부족...), 다른 방법을 찾다가 EventListener 을 통해 비동기 방식으로 처리할 수 있다고 하여 도전해봤다.

1. Events 작성

  • 해당 서비스에서 이벤트를 발생시킬 때 사용하는 Events 클래스

    object Events {
        private var eventPublisher: ApplicationEventPublisher? = null
        private val log = LoggerFactory.getLogger(this.javaClass)
    
        fun publishEvent(event: Any) {
            when (eventPublisher) {
                null -> {
                    log.warn("no eventPublisher, so don't publish an event.")
                }
                else -> {
                    eventPublisher!!.publishEvent(event)
                }
            }
        }
    
        fun setEventPublisher(eventPublisher: ApplicationEventPublisher) {
            Events.eventPublisher = eventPublisher
        }
    }
    • object 키워드를 통해 싱글톤 객체로 만들어줌
    • ApplicationEventPublisher 을 통해 이벤트를 퍼블리싱
    • eventPublisher 은 null 인 상태이므로 퍼블리싱 하기 전에 setEventPublisher 을 해줘야 함

2. EventsInitializer 생성

  • Events 에서 eventPublisher 을 주입해주기 위해 InitializingBean 을 사용

    @Configuration
    class EventsInitializer(val context: ApplicationContext) {
    
        @Bean
        fun initEvents(): InitializingBean {
            return InitializingBean { Events.setEventPublisher(context) }
        }
    }
    • 이렇게 하면 Spring Bean Lifecycle 에 의해 InitializingBean 이 실행이 되고, Events 객체의 eventPublisher 이 주입이 된다.

3. Events 를 사용할 Service 객체

  • Events 는 필요한 곳에서 사용하면 되지만, 이번에는 서비스 객체에서 사용

    @Service
    class ListService(private val provider: DataProvider) {
        fun callCachedData(status: String, req: RequestDTO) : PageInfo<DTO>? {
            Events.publishEvent(MakedKey(status, req))
            return provider.get(MakedKey(status, req))
        }
    }
    
    • Events.publishEvent() 를 통해 이벤트를 발생시킨다

4. EventListener 을 통해 이벤트 핸들러 구현

  • 이벤트를 발생을 시켰으니, 발생시킨 이벤트를 받는 곳을 구현해보자

    @Component
    class EventMetricHandler(private val meterRegistry: MeterRegistry) {
        private val log = LoggerFactory.getLogger(javaClass)
    
        @Async
        @EventListener(value = [MakedKey::class])
        fun handle(evt: MakedKey) {
            val tags =
                    listOf(
                            Tag.of("status", evt.status)
                    )
            val counter = meterRegistry.counter("counter", tags)
            counter.increment()
    
            log.info("increment metric: tag-{}", tags)
        }
    }
    • EventListener 을 통해 받은 이벤트를 처리할 수 있다.
    • status 라는 tag 를 생성해주고 각 tag 에 대한 카운트를 증가시켜준다.
    • 위의 경우에서는 list 라는 조회에 대해서 카운트를 하기 때문에 트랜잭션을 신경쓸 필요가 없지만, 저장 혹은 삭제했을 때에 대한 메트릭을 할 경우 @TransactionalEventListener 을 사용하면 된다. 해당 어노테이션의 경우 트랜잭션 커밋 이전 이후 등에 관한 설정을 할 수 있다.
    • 비동기 처리를 하기 위해서 @Async 어노테이션을 추가해주자.

5. @EnableAsync 추가

  • 어떻게 보면 비동기로 한다는 시점에서 처음 추가해줬어야 할 것 같은 설정이지만...

    @SpringBootApplication
    @EnableAsync
    class SampleEventApplication
    
    fun main(args: Array<String>) {
        runApplication<SampleEventApplication>(*args)
    }
    • @SpringBootApplication 어노테이션 자리에 @EnableAsync를 추가해주면 비동기 처리가 된다.

결론

  • 비동기 처리를 할 때 Events 객체를 추가해서 하는 방법은 좋은 방법인 것 같고 좋아보인다. 다만, 스프링을 사용하면서 좋은 기능은 정말 많은데, 모르는 것도 정말 많다는 것과 그만큼 알아야 하는 것도 정말 많다는 것을 할 때마다 느끼는 것 같다.

'개발 관련 > spring' 카테고리의 다른 글

Caffeine(캐시 라이브러리) 사용  (0) 2022.07.14
Feign 사용 (2)  (0) 2022.07.14
Feign 사용 (1)  (0) 2022.07.14
HandlerMethodArgumentResolver 사용  (0) 2022.07.14
JPA 어노테이션  (0) 2022.07.14