본문 바로가기
Spring

스프링 이벤트(Spring Event) 사용해보기

by hseong 2024. 2. 12.

스프링 이벤트를 사용하면 관심사를 분리하고 패키지 간의 의존성을 줄일 수 있습니다. 개인 프로젝트를 진행하면서 이래저래 사용하고 있어 짤막하게 정리해보려고 합니다.

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