스프링 이벤트를 사용하면 관심사를 분리하고 패키지 간의 의존성을 줄일 수 있습니다. 개인 프로젝트를 진행하면서 이래저래 사용하고 있어 짤막하게 정리해보려고 합니다.
1. 스프링 이벤트 개요
다음의 내용은 스프링 공식 문서 Standard and Custom Events 항목의 소개를 번역한 것입니다.
ApplicationContext
의 이벤트 핸들링은 ApplicationEvent
클래스와 ApplicationListener
인터페이스를 통해 제공됩니다. ApplicationListener
인터페이스를 구현하는 bean이 컨텍스트를 통해 배포되면 ApplicationEvent
가 컨텍스트에 발행될 때마다 bean이 알림을 받습니다. 이는 표준 옵저버 디자인 패턴입니다.
스프링 이벤트의 발행은 이벤트를 발행 -> 애플리케이션 컨텍스트에 배포 -> 이벤트 리스너에 알림 세 단계로 구성되어 있음을 알 수 있습니다.
Spring 4.2 버전 이전까지는 소개 문구와 같이 ApplicationEvent
클래스를 상속하여 이벤트 클래스를 만들어야 했지만 4.2 버전 이후부터는 이벤트 인프라가 개선되어 임의의 이벤트 객체 만들 수 있고 @EventListener
를 이용한 어노테이션 기반의 모델을 제공한다고 합니다.
ApplicationEventPublisher
를 확인해보면 PayloadApplicationEvent
가 임의의 이벤트 객체를 래핑해준다는 것을 확인할 수 있습니다.
2. 스프링 이벤트 사용법
Event
소개에서 설명드린 것과 같이 Spring 4.2 버전 이후부터는 ApplicationEvent
클래스를 상속하지 않아도 됩니다. 이벤트를 위해 필요한 정보만 담은 간단한 클래스를 작성해주면 됩니다.
public record CreateMemberEvent(Long memberId) {
}
EventPublisher
이벤트를 발행하기 위해서는 ApplicationEventPublisher
를 주입받아 publishEvent()
메서드를 호출하면 됩니다.
@Service
@RequiredArgsConstructor
public class AuthService {
private final MemberRepository memberRepository;
private final ApplicationEventPublisher eventPublisher;
...
@Transactional
public CreateMemberResponse createMember(CreateMemberCommand command) {
...
Member member = new Member(...);
memberRepository.save(member);
eventPublisher.publishEvent(new CreateMemberEvent(memberId)); // 회원 생성 이벤트 발행
return new CreateMemberResponse(member.getId());
}
EventListener
이벤트를 구독을 위해서는 @EventListener
를 사용하면 됩니다. 이벤트 클래스와 동일하게 Spring 4.2 버전 이후부터는 ApplicationListener
인터페이스를 직접 구현하는 대신 어노테이션을 이용하여 간편하게 이용할 수 있습니다.
@Component
@RequiredArgsConstructor
public class LinkBundleEventListener {
private final LinkBundleService linkBundleService;
@EventListener
public void createDefaultLinkBundle(CreateMemberEvent event) { // 회원 생성 이벤트 구독
...
linkBundleService.createLinkBundle(...);
}
}
3. TransactionEventListener
Spring 4.2 버전 이후부터는 @EventListener
외에 트랜잭션 단계에 따른 이벤트 구독을 위하여 @TransactionEventListener
를 제공합니다. 만일 회원가입이 성공적으로 이루어진 경우에만 실행되어야 하는 로직이 있다고 하면 다음과 같이 이벤트 리스너를 설정할 수 있습니다.
@Component
@RequiredArgsConstructor
public class LinkBundleEventListener {
private final LinkBundleService linkBundleService;
@TransactionalEventListener
public void createDefaultLinkBundle(CreateMemberEvent event) {
...
linkBundleService.createLinkBundle(...);
}
}
바인딩되길 원하는 트랜잭션 단계는 @TrasactionEventListener
의 phase 속성으로 지정할 수 있습니다.
BEFORE_COMMIT
AFTER_COMMIT
(기본값)AFTER_ROLLBACK
AFTER_COMPLETION
(트랜잭션 완료, 커밋 + 롤백)
만일 실행 중인 트랜잭션이 없는 경우에는 이벤트 리스너가 호출되지 않으므로 주의해야 합니다.
4. 주의할 점
@TrasactionEventListener
의 주석을 확인하면 다음과 같은 문구를 확인할 수 있습니다.
주석에서 설명한는대로 TransactionSynchronization.afterCompletion(int)
메서드의 주석을 확인해보면 좀 더 상세한 내용을 확인할 수 있습니다.
트랜잭션 리소스가 여전히 활성 상태이기 때문에 기존 트랜잭션에 참여하나, 해당 트랜잭션에서 더 이상 새로운 커밋을 수행하지 않으므로 새로운 변경 사항을 반영할 수는 없다. 라는 것을 알 수 있습니다. 따라서 동기적으로 처리하는 경우 트랜잭션 전파 옵션 중 하나인 Propagation.REQUIRES_NEW
를 사용해야 합니다.
@Component
@RequiredArgsConstructor
public class LinkBundleEventListener {
private final LinkBundleService linkBundleService;
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createDefaultLinkBundle(CreateMemberEvent event) {
...
linkBundleService.createLinkBundle(...);
}
}
REQUIRES_NEW
옵션을 사용하면 기존에 수행 중인 트랜잭션이 존재하더라도 참여하지 않고 항상 새로운 트랜잭션을 시작합니다. 따라서 새로운 변경사항이 DB에 정상적으로 반영됩니다.
이 외에도 @Async
를 사용하여 비동기적으로 처리할 수 있는 방법도 있습니다. 해당 내용은 스프링 공식 문서의 Asynchronous Listeners 참조하면 됩니다.
참고
이벤트 발행으로 얻을 수 있는 느슨한 의존성에 대한 설명은 조영호님의 우아한객체지향 세미나 영상을 추천드립니다.
https://www.youtube.com/watch?v=dJ5C4qRqAgA
좀 더 상세한 예제 및 이벤트 발행에 대해 알고 싶다면 배달의 민족 기술블로그에서 찾아볼 수 있습니다.
https://techblog.woowahan.com/7835/
그 외 참고 자료는 다음과 같습니다.
https://docs.spring.io/spring-framework/reference/core/beans/context-introduction.html#context-functionality-events
https://www.baeldung.com/spring-events
https://mangkyu.tistory.com/292
'Spring' 카테고리의 다른 글
스프링 @Async를 이용한 비동기 실행 적용하기 (1) | 2024.06.11 |
---|---|
스프링에서 외부 API 호출을 테스트하는 방법(feat. RestClient) (0) | 2024.05.20 |
로그 세팅하기 (1) | 2023.11.19 |
@WebMvcTest와 테스트 코드 개선하기 (0) | 2023.10.04 |
낙관적 락과 동시성 테스트하기 (0) | 2023.09.16 |