2 분 소요

동기

@EventListener가 붙어있는 로직을 수정할 일이 생겼었는데, Listener가 붙어있으니까 kafka 아닌가? 아니면 비동기 처리를 하는 무언가일까? 라고 간단하게 생각하고 열심히 비동기만 검색하며 안타깝게 시간을 보낸적이 있었다..! 이후에 찾아보니 이벤트를 만들어서 처리하는 그런 방식은 맞지만 비동기 방식은 아니라는걸 알게됐다. 기왕 공부하는 김에 적어놓고 싶었다.

@EventListener

개념

@EventListener는 말 그대로 이벤트를 들으려고(처리하려고) 기다리는 녀석이다!
publisher-listener 방식으로 동작하고, publisher 쪽에서는 ApplicationEvent.publishEvent()로 이벤트를 발행한다.
발행된 이벤트를 @EventListener가 붙어있는 메소드에서 처리한다고 보면 되겠다.

kafka도 publisher-listener 관계긴 하지만 위처럼 발행한 이벤트를 직접 listener가 바로 받는 방식은 아니고 topic에 넣어주게 된다. 그러면 listener(=consumer)는 구독하고 있는 topic에 메시지가 있는지 확인하고 꺼내서 처리하는 거라고 보면 된다.

왜 쓰나요?

강한 결합(=의존성이 높은) 구조를 느슨한 결합으로 풀어낼 수 있다!
무슨 말인지 모르겠다면 아래 예제를 보자.

TestService는 TestService2, TestService3를 주입받아서 superComplexLogic()에 활용하고 있다.
그런데.. 코드가 너무너무 복잡해졌다고 해보자! 이젠 코드만 쭉 읽어서는 이해하기가 어려운 지경에 왔다고 해보자.

@Service
class TestService(
    private val testService2: TestService2,
    private val testService3: TestService3,
) {
    fun superComplexLogic() {
        // testService2로 엄청 복잡한 로직을 수행
        // ...
        // testService3로 엄청엄청 복잡한 로직을 수행
    }
}
  • TestService는 TestService2, TestService3와 강한 결합을 맺고 있음

이러면 testService2 로직에 에러가 생겼을 때 superComplexLogic()으로 전파되고, testService3 역시 마찬가지다.
여기서는 예시를 위해 간단하게 작성했지만, 로직이 커지고 복잡해질 수록 에러도 잦아질거고 원인 추적도 힘들어질거다.

적용해보기

그러면 위 예시를 느슨한 결합으로 만들어보자!
일단 이벤트를 발행할 TestService에서는 ApplicationEventPublisher를 주입받아야 한다. 그리고 publishEvent()를 호출해주면서 파라미터를 넘기고 싶은 객체를 넣어주면 된다. TestService2가 TestService2Data를 사용한다고 가정하고, TestService3가 TestService3Data를 사용한다고 가정해보자.

@Service
class TestService(
    private val applicationEventPublisher: ApplicationEventPublisher,
) {
    fun superComplexLogic() {
        applicationEventPublisher.publishEvent(
            TestService2Data("test2")
        )
        
        applicationEventPublisher.publishEvent(
            TestService3Data("test3")
        )
    }
}


이제 @EventListener를 써보자! 일단 listener 역할을 할 TestListener에 @Component를 적용해 컴포넌트 스캔 대상이 되도록 설정해주고, 위에 superComplexLogic()에서 처리하던 내용들을 그대로 가져오면 된다.

그러면 superComplexLogic()를 호출해도 testService2, testService3에서 발생하는 에러는 더 이상 superComplexLogic()로 전파되지 않고, 원인을 찾으러 탐색할 필요가 없어지게 된다. 즉, TestListener는 각 서비스들과 느슨한 결합 완성!

@Component
class TestListener(
    private val testService2: TestService2,
    private val testService3: TestService3,
) {

    @EventListener
    fun complexLogicForTestService2(data: TestService2Data) {
        // testService2로 엄청 복잡한 로직을 수행
    }

    @EventListener
    fun complexLogicForTestService3(data: TestService3Data) {
        // testService3로 엄청엄청 복잡한 로직을 수행
    }
}
  • 발행된 이벤트에 담긴 객체 타입에 따라 호출되는 함수가 달라짐 (listener 내 메소드 파라미터에 주목)
    • TestService2Data -> complexLogicForTestService2()
    • TestService3Data -> complexLogicForTestService3()
  • TestService 뿐만 아니라 testService2, testService3 로직을 사용하는 모든 곳에서 사용할 수도 있다는 장점이 있음

그래서 처리 방식은 동기일까, 비동기일까?

기본적인 처리 방식은 동기가 맞다! 아래 테스트 과정을 보자.

간단한 테스트를 위해 API를 구현해줬다. 호출하면 바로 이벤트를 발행하는 구조다.

@RestController
class TestController(
    private val applicationEventPublisher: ApplicationEventPublisher,
) {
    private val log = LoggerFactory.getLogger(this.javaClass)

    @GetMapping("/test")
    fun test(): ResponseEntity<*> {
        log.info("[before-publish] current thread id : ${Thread.currentThread().id}")
        applicationEventPublisher.publishEvent(
            TestData("testMessage")
        )
        log.info("[after-publish] current thread id : ${Thread.currentThread().id}")
        return ok().body("API call success")
    }
}


위에서 발행한 이벤트는 여기 listener에서 받는다.

@Component
class TestListener {
    private val log = LoggerFactory.getLogger(this.javaClass)

    @EventListener
    fun handleTestData(testData: TestData) {
        log.info("thread id : ${Thread.currentThread().id} request: ${testData.message}")
    }
}


API 호출 시 찍힌 로그를 확인해보면, listener로 넘어갔을 때 스레드를 그대로 사용하는걸 확인할 수 있다.

?

여기서 listener도 비동기로 동작하게 만들 수 있다! 하지만 트랜잭션에 유의해서 작성해줘야한다. 자세한 건 아래 글을 참조하자!

Reference

카테고리:

업데이트:

댓글남기기