Spring EventListener: 이벤트 리스너란 무엇인가
TOC
- Overview
- Event란 무엇인가
- 왜 직접 메서드를 호출하지 않는가
- 기본 실행 방식과 내부 동작 흐름
- 구성 요소
- 구조적 한계
- Transaction과 동기·비동기 실행
- 최소 코드 예제
- 요약
Overview
애플리케이션을 개발하다 보면 하나의 작업이 끝난 뒤 다른 작업을 이어서 실행해야 하는 경우가 많다. 회원가입 후 환영 메시지를 보내거나 상품 정보가 변경된 뒤 캐시를 삭제하는 작업이 대표적이다.
가장 단순한 방법은 필요한 서비스를 직접 호출하는 것이다.
userService.create(user);
notificationService.sendWelcomeMessage(user);
하지만 후속 작업이 늘어날수록 하나의 서비스가 너무 많은 구현을 알아야 한다. 이때 Spring의 Application Event와 @EventListener를 사용할 수 있다.
Event란 무엇인가
Event는 이미 발생한 사건을 표현하는 객체다.
public record UserRegistered(Long userId) {
}
UserRegistered는 사용자가 등록되었다는 사실을 표현한다. Event는 동작을 수행하는 객체가 아니라, 이미 발생한 사실과 후속 처리에 필요한 데이터를 전달하는 객체다. 이름은 보통 UserRegistered, OrderCompleted처럼 과거형으로 작성한다.
왜 직접 메서드를 호출하지 않는가
직접 호출 방식에서는 회원가입 서비스가 알림 서비스의 존재와 메서드 이름을 알고 있어야 한다.
userService.create(user);
notificationService.sendWelcomeMessage(user);
auditService.write(user);
cacheService.evict(user.id());
후속 작업이 추가될 때마다 핵심 서비스가 수정된다. Event를 사용하면 회원가입 서비스는 “사용자가 등록되었다”는 사실만 발행하고, 메일 발송이나 감사 로그는 각 listener가 담당한다.
기본 실행 방식과 내부 동작 흐름
publishEvent()가 호출되면 Spring은 등록된 listener를 찾아 기본적으로 이벤트를 발행한 thread에서 동기 실행한다.
Publisher
|
| publishEvent()
v
Spring Event Infrastructure
|
+-- Listener A
+-- Listener B
listener가 오래 걸리면 publisher의 실행도 지연된다. listener가 여러 개라면 기본 호출 순서를 업무 순서라고 가정하지 말고, 필요한 경우 @Order를 사용한다.
구성 요소
Spring 이벤트 흐름은 이벤트를 발행하는 Publisher, 전달할 Event, 이벤트를 처리하는 Listener로 구성된다.
Publisher
Publisher는 이벤트가 발생했다는 사실을 ApplicationEventPublisher를 통해 Spring 이벤트 인프라에 전달한다.
Event
Event는 publisher와 listener 사이에서 전달되는 사실과 처리에 필요한 최소 데이터를 담는다. JPA Entity 전체보다 ID와 필요한 값만 담는 편이 안전하다.
Listener
Listener는 특정 이벤트 타입을 구독하고 이벤트가 발행되었을 때 후속 작업을 수행한다.
@EventListener
public void handle(UserRegistered event) {
notificationService.sendWelcomeMessage(event.userId());
}
구조적 한계
이벤트는 직접 호출 결합도를 줄이지만 이벤트 타입이라는 계약은 공유한다. 또한 호출 흐름을 추적하기 어렵고, 기본 동기 실행으로 publisher의 응답 시간에 영향을 주며, 이벤트를 영속적으로 저장하거나 재시도하지 않는다.
Transaction과 동기·비동기 실행
@Transactional 서비스가 동기 listener를 호출하면 listener의 DB 작업은 같은 thread의 현재 transaction에 참여할 수 있다. 그러나 listener는 commit 이후가 아니라 publishEvent() 호출 시점에 실행된다.
@Transactional service
-> publishEvent()
-> 동기 listener: 같은 thread, 현재 transaction에 참여 가능
-> commit 또는 rollback
listener가 @Async로 다른 thread에서 실행되면 transaction context가 전파되지 않으므로 원래 transaction을 공유하지 않는다. 해당 메서드에 @Transactional이 있으면 프록시 호출을 전제로 별도 transaction이 시작될 수 있다.
최소 코드 예제
다음 코드는 회원가입 이벤트를 발행하고 listener가 사용자 식별자를 받아 환영 메시지를 보내는 최소 흐름이다.
public record UserRegistered(Long userId) {
}
@Service
@RequiredArgsConstructor
public class UserService {
private final ApplicationEventPublisher publisher;
private final UserRepository userRepository;
@Transactional
public void register(User user) {
userRepository.save(user);
publisher.publishEvent(new UserRegistered(user.id()));
}
}
@Component
@RequiredArgsConstructor
public class WelcomeMessageListener {
private final NotificationService notificationService;
@EventListener
public void handle(UserRegistered event) {
notificationService.sendWelcomeMessage(event.userId());
}
}
이 구조에서 UserService는 알림 서비스의 구현을 직접 호출하지 않는다. 단지 사용자가 등록되었다는 이벤트를 발행하고, 알림 처리는 listener가 담당한다.
요약
@EventListener는 직접 호출을 줄이는 Spring 내부 이벤트 도구다. Event는 발생한 사실을 표현하고, Publisher는 이벤트를 발행하며, Listener는 후속 작업을 수행한다.
기본 listener는 같은 thread에서 동기 실행되므로 listener의 DB 작업은 현재 transaction에 참여할 수 있다. 하지만 commit 이후 실행되는 것은 아니며, @Async를 사용하면 원래 transaction을 공유하지 않는다.
이벤트 전달의 영속성이나 재시도가 필요하다면 @EventListener만으로는 부족하다. transaction 성공 이후에만 처리해야 하는 경우에는 @TransactionalEventListener를 검토해야 한다.