코틀린 스프링에서의 이벤트 처리 (with 유스콘)

728x90

들어가기에 앞서서...

| 해당 글은 2021 유스콘에서 발표한 자바 스프링에서의 이벤트 처리 세션에서 진행된 내용을 코틀린으로 따라해보며 정리한 글입니다.

목적

| 스프링에서 이벤트를 왜 써야하고 어떻게 써야하는지 실습해본다.

전체적인 코드는 깃헙 저장소를 참고하면 된다.

이 코드를 이벤트 형식으로 변경하여 결합이 느슨해지도록 하는 것이 목표이다!

`UserService쪽에 의존하는 다양한 의존성들을 어떻게 해결하면 좋을지?`를 계속 생각해보자!

Spring은 Bean을 관리하기 위해서 ApplicationContext를 기본으로 가져간다.
이번에 다루게 될 Event도 Spring Bean과 똑같이 Context를 관리해주는데

AbstractApplicationContext class 내부 코드

이벤트 또한 ApplicationContext에서 Event를 관리하고 있다.
따라서 Event도 특별한 Bean을 주입받고 실행해주고 있다.

다이어 그램으로 본 모습
EventPublisher

Step1: 상속 기반의 이벤트로 변경 해본다.

Event 객체는 ApplicationEvent를 EventListener는 ApplicationListener 인터페이스를 상속 및 구현하여 변경한다.

일부 코드만 기재하겠다.

UserAdminEvent 객체

class UserAdminEvent(
    source: Any,
    val userName: String
) : ApplicationEvent(source)

UserAdmin

@Component
class AdminEventListener : ApplicationListener<UserAdminEvent> {
    override fun onApplicationEvent(event: UserAdminEvent) {
        logger.info("어드민 서비스: ${event.userName} 님이 가입했습니다.")
    }

    companion object {
        private val logger = LoggerFactory.getLogger(AdminEventListener::class.java)
    }
}

비교하기 쉬우라고 이렇게 메서드를 매번 호출했다. 하나의 메서드로 묶어서 한 번만 호출하도록 캡슐화하는 것을 추천한다!

Step1을 완료했다 👏👏👏👏

이전 코드와 큰 차이점은 기존의 dependency가 있었던 Service들이 많이 사라졌다. 
이런 부분들을 Event를 활용해서 Coupling(= 결합도)를 많이 줄일 수 있다는 것이 큰 장점이다.

위 방식의 문제점은 무엇일까? 
매번 상속 혹은 구현하고 Component를 등록해주는 절차가 필요하다. 
스프링 4.2 부터 ApplicationEvent 를 상속 받아 객체를 생성하지 않아도 이벤트 객체 처럼 활용할 수 있다.  
ApplicationListenerMethodAdapter 객체에서 어노테이션을 찾아서 실행한다!

위와 같이 Event 객체와 EventListener 객체는 모두 상속(or 구현)을 하고있다! 
이러면 Event에 종속적인 것이 아닌가? 라는 의문을 품어볼 수도 있다. 
이런 문제점을 스프링 4.2부터는 애너테이션을 통해서 개선했다. 

애너테이션 기반으로 변경되면서 Event 객체는 별도의 Bean으로 관리되지 않고 순수 자바 객체를 사용할 수 있다. 
EventListener의 메서드에 @EventListener 애너테이션을 붙임으로써 Event를 읽어들여 응답할 수 있다. 

예제코드는 아래와 같다. 

@Component
class UserSenderEventListener {
    @EventListener
    fun onApplicationEvent(event: UserSenderEvent) {
        logger.info("환영 이메일 발송 성공 : ${event.email}")
    }

    @EventListener
    fun handleSMS(event: UserSenderEvent) {
        logger.info("환영 SMS 발송 성공: ${event.phoneNumber}")
    }

    companion object {
        private val logger = LoggerFactory.getLogger(UserSenderEventListener::class.java)
    }
}

또 다른 차이점이 있다면 이전에는 하나의 클래스당 하나의 EventListener를 만들 수 밖에 없었지만, 애너테이션 방식에서는 하나의 클래스에서 여러 EventListener를 만들 수 있다. 

또한 상속을 받는 것이 아니기 때문에 테스트 코드를 작성했다면, 테스트 코드 또한 변경이 필요하다! (예제코드 참고!)

여기서 끝이라고 생각할 수 있지만, 한 가지 간과한 부분이 있다. 
우리는 UserService 클래스 상단에 @Transactional 애너테이션을 붙였기 때문에, 메서드가 온전히 다 실행되거나 혹은 하나도 실행되지 않거나 하는 즉, 원자성을 보장한다고 생각할 것이다. (트랜잭션을 조금이라도 공부해봤다면 정말 심각한 일이라는 것을 알 것이다! ex. 은행 송금 상황 시 통장에서 돈만 인출되고 상대방에게 입금은 안 된 경우) 

이 문제는 EventListener를 (애너테이션으로)등록할 때, @EventListener가 아닌 @TransactionalEventListener으로 바꿔서 사용하면 된다. @TransactionalEventListener 의 phase 속성을 상황에 맞게 알맞게 설정해야한다. (default 설정은 TransactionPhase.AFTER_COMMIT 이고, 그 외에는 AFTER_ROLLBACK, AFTER_COMPLETION, BEFORE_COMMIT 등 총 4가지 속성이 있다)

Step2를 더 넘어서서 도메인 이벤트를 활용해보는 방법도 있다.
해당 개념에 대해서는 예제 코드의 INDEX.md 의 내용을 참고해서 스스로 학습해보면 좋을 것 같다.

Evenet의 단점은 Listener와 Event Object를 관리해야한다는 것이다. 
하지만 그 만큼 역할도 명확해지고 UserService에 대한 의존성, 결합성들이 해소될 수 있다는 장점이 크다. 
이런 부분들을 잘 고려해서 Event를 선택해서 사용하도록 하자! 

728x90