[Spring] 스프링 4.2 이후 version 에서 Application Event 도입을 해보았습니다.
왜 Application Event 를 도입하기로 하였는가.
ElasticSearch를 사용하여 검색 기능을 구현하면서, 엔티티를 생성하거나 수정할 때 해당 엔티티의 인덱스를 생성 및 수정하는 작업도 함께 수행해야 했습니다. 기존 코드에서는 User 객체를 생성할 때 UserService에서 UserIndexService를 의존성 주입받아 사용하며, User 객체 생성 후 인덱스를 생성하는 방식으로 구현되었습니다.
class UserService(
private val userIndexService: UserIndexService
) {
@Transactional
fun create(name: String, identity: Identity) : User {
val user = User(name = "kdmstj", identity = Identity.STUDENT)
useRepository.save(user)
userIndexService.create(user)
}
}
높은 결합도 문제: UserService와 UserIndexService 간의 강한 의존성은 시스템의 복잡성을 증가시키고, 유지 보수 시 다양한 문제를 야기할 수 있습니다. 이러한 결합도는 코드의 유연성과 확장성을 저해합니다.
트랜잭션 관리 문제: User 생성과 UserIndex 생성이 동일한 트랜잭션 내에서 동작하면서, 만약 인덱스 생성이 실패할 경우 전체 트랜잭션이 롤백되어 User 생성도 실패하게 됩니다. 이 문제는 기획에 따라 다를 수 있지만, 인덱스 생성 실패가 User 생성 실패로 이어지는 것이 바람직한지에 대한 의문이 제기됩니다.
이러한 문제를 해결하기 위해, Spring Application Event를 도입했습니다. 이를 통해 서비스 간의 결합도를 줄이고, 트랜잭션 관리 문제를 개선할 수 있었습니다.
Application Event 도입 방법
Spring Event 는 3가지 요소가 있습니다.
1. 이벤트를 처리하는데 필요한 데이터가 담긴 event class
2. 이벤트를 발생시키는 event publisher
3. 이벤트를 받아들이는 evenet listener
하나씩 차례대로 구현해보겠습니다. 저는 스프링 부트 3.2 버전을 사용하고 있습니다.
1. 이벤트를 처리하는데 필요한 데이터가 담긴 event class
먼저, 이벤트를 처리해야 할 데이터를 담는 event class를 정의합니다. 예를 들어, User 객체가 생성된 후 인덱스 생성 작업을 처리하기 위한 이벤트 클래스를 작성할 수 있습니다.
class UserIndexCreatedEvent(
val user: User
)
2. 이벤트를 발생시키는 event publisher
이벤트를 발행하는 것은 ApplicationEventPublisher 인터페이스를 통해 이루어집니다. ApplicationEventPublisher는 스프링 컨텍스트에서 관리되는 빈으로, 이벤트 발행만을 담당합니다.
class UserService(
private val userIndexService: UserIndexService,
private val applicationEventPublisher: ApplicationEventPublisher
) {
@Transactional
fun create(name: String, identity: Identity) : User {
val user = User(name = "kdmstj", identity = Identity.STUDENT)
useRepository.save(user)
applicationEventPublisher.publishEvent(UserCreatedEvent(user))
}
}
위 코드에서 publishEvent() 메서드를 통해 UserCreatedEvent 객체를 전달하여 이벤트를 발행합니다.
3. 이벤트를 받아들이는 event listener
발행된 이벤트 는 EventListener 객체가 수신합니다. 이 객체는 스프링에서 관리되며, 특정 이벤트가 발생하면 등록된 리스너 메서드가 호출됩니다.
@Component
class UserEventListener(
val userIndexService: UserIndexService,
) {
@EventListener
fun onApplicationEvent(event: UserCreatedEvent) {
userIndexService.createIndex(event.user)
}
}
@Component 어노테이션을 사용하여 리스너를 스프링 빈으로 등록하고, @EventListener 어노테이션을 통해 이벤트 발생 시 호출되는 메서드를 정의합니다. 이렇게 하면 User를 생성하고 UserIndex를 생성하는 과정이 별도의 트랜잭션으로 분리될 수 있습니다. 만약 UserIndex 생성이 실패하더라도 User 생성이 롤백되지 않게 됩니다.
그렇다면 user 를 생성하는 것과 userIndex 를 생성하는 것 트랜잭션으로 묶고 싶지 않은 경우 어떻게 해야할까요? 즉, 비즈니스 트랜잭션과는 무관하게 좀 더 느슨하게 이벤트를 처리하고 싶은 경우 어떻게 해야할까요?
비즈니스 트랜잭션과의 분리 : @TransactionalEventListener
User 생성과 UserIndex 생성이 같은 트랜잭션으로 묶이고 싶지 않은 경우, 즉 비즈니스 트랜잭션과는 독립적으로 이벤트를 처리하고자 한다면 @TransactionalEventListener를 사용할 수 있습니다.
@Component
class UserEventListener(
val userIndexService: UserIndexService,
) {
@TransactionalEventListener
fun onApplicationEvent(event: UserCreatedEvent) {
userIndexService.createIndex(event.user)
}
}
@TransactionalEventListener는 기본적으로 AFTER_COMMIT 옵션을 사용하여, 트랜잭션이 성공적으로 커밋된 후에 리스너가 동작하도록 설정됩니다. 이 설정을 통해 User 생성이 성공적으로 완료된 이후에만 인덱스 생성이 시도되며, 인덱스 생성 실패로 인해 User 생성이 영향을 받지 않게 됩니다.
해당 어노테이션은 여러가지 옵션을 가지게 됩니다.
- AFTER_COMMIT (default) : 트랜잭션이 성공한 후 이벤트 발생
- AFTER_ROLLBACK : 트랜잭션이 롤백된 경우 이벤트 발생
- AFTER_COMPLETION : 트랜잭션이 완료된 경우 발생 (AFTER_COMMIT, AFTER_COMPLETION)
- BEFORE_COMMIT : 트랜잭션 커밋 직전에 이벤트 발생
@TransactionalEventListener 을 사용할 때 주의해야 할 점이 있습니다. (제가 삽질을 했던 포인트이기도 하구요..)
만약 이벤트 리스너에서 추가로 데이터베이스에 insert, update, delete 작업을 진행해야 하는 상황이라면, 기본 설정으로는 해당 작업이 커밋되지 않을 수 있습니다. 이는 이벤트 리스너가 이벤트를 발행한 트랜잭션 내에서 동작하기 때문입니다.
이를 해결하기 위해 @Transactional(propagation = Propagation.REQUIRES_NEW)를 사용하여 새로운 트랜잭션을 생성하도록 설정할 수 있습니다.
@Component
class UserEventListener(
val userLogRepository: UserLoagRepository
) {
@TransactionalEventListener
@Transactional(propagation = Propagatoin.REQUIRES_NEW)
fun onApplicationEvent(event: UserCreatedEvent) {
userLogRepository.save(event.user)
}
}
이 설정을 통해 이벤트 리스너에서 추가적인 데이터베이스 작업을 안전하게 수행할 수 있습니다.
Spring 4.2 이전 버전에서는 주의해야 합니다.
Spring 4.2 이전 버전에서는 이벤트 객체가 ApplicationEvent를 상속받아야 했고, EventListener는 ApplicationListener<발행될 객체 타입>을 구현해야 했습니다.
이후 버전에서부터는 ApplicationEventPublisher interface 에 ApplicationEvent 를 상속받은 객체뿐만이 아니라 Object 타입의 모든 객체를 인자로 받는 publishEvent() 가 추가되었기 때문에 ApplicationEvent 를 상속받지 않아도 되고, @EventListener 를 붙여서도 구현이 가능하도록 되었습니다.
개발 중에 시스템의 확장성과 유지 보수성을 높이기 위해 코드의 결합도를 줄이는 것은 중요합니다. 특히, 특정 서비스가 다른 서비스에 의존할 경우, 이러한 결합도는 시스템의 증가시키고 변경사항이 있을 때 문제가 발생할 가능성을 높입니다. 이를 해결하기 위해서 스프링의 Spring Appliation Event 를 도입하였고, 이 글에서는 그 과정과 방법을 설명하였습니다. 블로깅을 하면서 느끼는 거지만 버전에 맞는 코드를 작성하는 것이 정말 중요한 것 같습니다.