안녕세계

[Spring] EventListener vs TransactionalEventListener 본문

[Spring] EventListener vs TransactionalEventListener

Junhong Kim 2023. 3. 8. 22:32
728x90
반응형

EventListener vs TransactionalEventListener

스프링 4.2 이상 버전에서 @EventListener@TransactionalEventListener 두 가지로 애플리케이션 이벤트를 수신할 수 있습니다. 두 어노테이션은 리스너 어노테이션이 설정된 메서드 인자의 이벤트 객체를 받아서 처리한다는 공통점이 있지만 약간의 차이 점이 존재합니다. 이번 포스팅에서는 두 어노테이션의 사용 방법과 차이 점을 살펴봅니다. 😊

EventListener

우선, EventListener는 애플리케이션에서 모든 타입의 이벤트를 수신하는 가장 단순한 방법입니다. EventListener는 트랜잭션에 참여하지 않으므로 트랜잭션 관련된 동작이 없으며 동기적으로 실행됩니다. 동기적으로 수행한다는 의미는 이벤트를 발행하는 순간에 리스너가 동작한다고 이해하면 쉽습니다. 😆

@Component
class MemberListener {
    @EventListener
    fun litener(message: String) {
        log.info { "$message" }
    }
}

@Service
class MemberService(
    private val memberRepository: MemberRepository,
    private val applicationEventPublisher: ApplicationEventPublisher
) {
    @Transactional
    fun signUp(dto: MemberSignUpRequest) {
        applicationEventPublisher.publishEvent("welcome") // 이벤트 발행 즉시 리스너 동작
        log.info { "-- signUp end --" }
    }
}

// 결과
2023-03-07 21:46:59.650  INFO 83026 --- [nio-8080-exec-2] c.e.s.event.listener.SignUpListener      : welcome
2023-03-07 21:46:59.651  INFO 83026 --- [nio-8080-exec-2] c.e.springdemo.domain.MemberService      : -- signUp end --

위 예제의 로그를 살펴보면 이벤트를 발행한 즉시 welcome 로그가 출력되었고 이후 signUp end 로그가 출력된 것을 확인할 수 있습니다. 이처럼 EventListener는 동기적으로 수행된다는 것을 확인할 수 있습니다.

TransactionalEventListener

TransactionalEventListener는 트랜잭션 동작을 인식하는 리스너입니다. 이는 트랜잭션 컨텍스트 내에서 보내는 이벤트를 수신하는데 사용됩니다. 따라서 트랜잭션이 없다면 리스너가 작동하지 않습니다. 또한, TransactionalEventListener는 트랜잭션의 4가지 동작을 인식할 수 있으며 각 단계의 의미는 다음과 같습니다.

  • TransactionPhase.BEFORE_COMMIT [default] 
    • 트랜잭션 commit 직전에 수행 됨 (주의! 트랜잭션 직입 직전이 아님)
  • TransactionPhase.AFTER_COMMIT
    • 트랜잭션 commit 직후에 수행 됨
  • TransactionPhase.AFTER_ROLLBACK
    • 트랜잭션 rollback 직후에 수행 됨 (=트랜잭션 rollback이 일어난 시점에 이벤트가 실행 됨)
  • TransactionPhase.AFTER_COMPLETION
    • 트랜잭션이 완료된 뒤 수행 됨 (commit 또는 rollback)
@Component
class MemberListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    fun listener(message: SignUpMessage) {
        log.info { "${message.contents} from ${message.from}" }
    }
}

@Service
class MemberService(
    private val memberRepository: MemberRepository,
    private val applicationEventPublisher: ApplicationEventPublisher
) {
    @Transactional
    fun signUp(dto: MemberSignUpRequest) {
        val savedMember = memberRepository.save(dto.toEntity())
        val signUpMessage = SignUpMessage("welcome", savedMember.name);
        applicationEventPublisher.publishEvent(signUpMessage)
        log.info { "-- signUp end --" }
    }
}

// 결과
2023-03-07 23:16:53.247  INFO 94060 --- [nio-8080-exec-1] c.e.springdemo.domain.MemberService      : -- signUp end --
2023-03-07 23:16:53.251  INFO 94060 --- [nio-8080-exec-1] c.e.s.event.listener.SignUpListener      : welcome from junhong

위 예제의 로그를 살펴보면 signUp end 로그 이후에 이벤트 리스너가 실행 된 것을 확인할 수 있습니다. TransactionalEventListener는 트랜잭션 동작을 인식하기 때문에 이벤트 실행 시점이 코드가 작성된 순서와 다른 것을 확인할 수 있습니다.

주의사항

1. Kotlin에서 이벤트 객체로 Int(null 비허용 타입)를 발행하면 리스너에서 매개변수 Int(null 비허용 타입)로 수신할 수 없습니다.

@Service
class MyEventService(
	private val applicationEventPublisher: ApplicationEventPublisher
) {
	@Transactional
	fun pubish(dto: SignUpRequest) {
	  applicationEventPublisher.publishEvent(1) // 이벤트 객체 1 발행
	}
}

@Component
class MyEventListener {
	@EventListener
	fun listen(message: Int) { // 이벤트 객체 1에 대한 수신이 가능할까?
	    log.info { "message = ${message}" }
	}
}

위 예제를 실행해보면 예상과는 다르게 java.lang.IllegalArgumentException: argument type mismatch 에러가 발생합니다. 이유가 무엇일까요? Kotlin에서 사용된 Int는 Java에서는 원시 타입 int로 해석됩니다. 이는 Kotlin Int가 null을 허용하지 않기 때문에 Java에서 원시타입으로 변환해준 것입니다. 따라서, publishEvent()는 원시타입을 받을 수 없기 때문에 에러가 발생한 것 입니다.

 

그렇다면 Int 값을 이벤트 객체로 어떻게 처리해야할까요? Kotlin 코드의 리스너에서 이벤트 객체를 null 비허용 타입 Int가 아닌 Int?로 받으면 됩니다. Kotlin의 Int? 는 Java에서 wrapper 클래스 Int로 변환되므로 정상 동작하는 것을 확인할 수 있습니다. 🙌

@Component
class MyEventListener {
	@EventListener
	fun listen(message: Int?) { // Int -> Int? 로 수정
	    log.info { "message = ${message}" }
	}
}

2. AFTER_COMMIT 단계에서 JPA entity를 수정하더라도 entity에 반영되지 않습니다.

왜냐하면 기존 트랜잭션에서 이미 commit이 완료된 상태이기 때문입니다. 이와 같은 맥락으로 AFTER_COMMIT 단계에서 신규 트랜잭션을 생성하더라도 실행되지 않습니다.

 

3. EventListener를 비동기 처리하지 않으면, 이벤트 처리가 종료될 때까지 DB 커넥션을 물고 있습니다.

따라서, 이벤트를 비동기 처리해도 무방한 경우 @Async를 사용하여 요청에 대한 응답을 빠르게 반환하여 DB 커넥션을 반환하는 것을 권장합니다. 

@Component
class MyEventListener {

    @Async // Async가 붙어있어서 이벤트가 비동기로 처리됨
    @EventListener
    fun listen(message: String) {
        try {
            Thread.sleep(5000)
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
        log.info { "$message" }
    }
}

결론

EventListener와 TransactionalEventListener 모두 애플리케이션 이벤트를 수신하는데 사용할 수 있습니다. EventListener는 트랜잭션 동작을 인식하지 않는 단순한 리스너인 반면에 TransactionalEventListener는 트랜잭션 동작을 인식하는 리스너입니다. 두 가지 리스너 중 어느것을 선택해야할지는 애플리케이션 요구사항에 따라 달라지므로 둘의 차이를 인지하고 필요에 따라 사용하시길 바랍니다. 👋

참고

728x90
반응형
Comments