ApplicationEventPublisher 란?
- ApplicationEventPublisher는 ApplicationContext가 상속하는 인터페이스 중 하나이다.
- ApplicationContext에는 ApplicationEventPublisher 인터페이스가 이미 구현되어있다.
- 그렇기 때문에 ApplicationEventPublisher를 이용해서 쉽게 이벤트를 발생시키고 처리할 수 있다.
- Design Pattern 중 하나인 옵저버 패턴의 구현체이다.
- 또한, Event를 발행해서 중첩되는 Transactional 내부에서 Commit 시점을 핸들링 할 수도 있다.
이벤트 처리 구조
- 먼저 이벤트를 정의한다.
- Publisher 를 통해서 이벤트를 발생시킨다.
- Listener 를 통해서 이벤트를
- 이벤트를 발행한다.
예시
이벤트 정의
public class Event {
private String eventName;
public Event(String eventName) {
this.eventName = eventName;
}
public String getEventName() {
return eventName;
}
}
- 먼저 위와 같이 이벤트를 정의한다. (Spring4.2 이전에는 ApplicationEvent를 상속받아야 했다.)
- 이벤트는 POJO로 구현하는 것이 좋다.
- 그렇기 때문에 상속, 인터페이스 구현, 애노테이션을 사용하지 않는 것을 권장한다.
- 그렇게 해야 테스트하기 쉽고 유지보수 하기 용이하다.
Publisher 구현
@Component
@RequiredArgsConstructor
public class EventPublisher {
private final ApplicationEventPublisher eventPublisher;
public void applyEvent(Event event) {
eventPublisher.publishEvent(event);
}
}
- ApplicationEventPublisher 클래스의 publishEvent(Object) 메소드를 통해서 이벤트를 발생시킬 수 있다.
- publishEvent 는 Object를 받을 수 있기 때문에 어떤 타입이든 받을 수 있다.
이벤트 리스너 정의
@Component
public class EventHandler {
@EventListener
public void handle(Event event) {
System.out.println("이벤트 명: " + event.getEventName())
}
}
- 먼저 EventHandler 를 빈으로 등록해 줘야한다.
- 스프링이 누구에게 이벤트를 전달할지 알아야하기 때문이다.
- @EventListener 를 통해서 이벤트를 받을 수 있다.
- 이벤트가 발생하면 @EventListener가 붙은 메소드가 실행된다.
비동기 처리
@Component
public class EventHandler {
@EventListener
@Async
public void handle(Event event) {
System.out.println("이벤트 명: " + event.getEventName())
}
}
- 비동기 처리를 하고 싶다면 @Async 를 붙여주면 된다.
- 물론, SpringApplication.run을 실행하는 Class에 @EnableAsync 통해서 비동기 임을 알려야한다.
순서 지정
- 이벤트 처리 순서를 지정하고 싶다면, @Order를 추가하면 된다.
@Component
public class EventHandler {
@EventListener
@Order(Ordered.HIGHEST_PRECEDENCE)
public void firstHandle(Event event){}
@EventListener
@Order(Ordered.HIGHEST_PRECEDENCE+1)
public void secondHandle(Event event){}
@EventListener
@Order(Ordered.LOWEST_PRECEDENCE)
public void lastHandle(Event event){}
}
- 위와 같이 우선순위를 지정해서 리스너를 실행시킬 수 있다.
- @Order안에 작은 값을 갖을 수록 우선권을 갖는다.
중첩된 @Transactional에서 rollback 핸들링 하기
public class MemberSignUpService {
private final MemberRepository memberRepository;
private final CouponIssueService couponIssueService;
private final EmailSenderService emailSenderService;
@Transactional
public void signUp(final MemberSignUpResquest dto) {
final Member member = memberRepository.save(dto.toEntity());
emailSenderService.sendSignUpEmail(member);
couponIssueService.issueSignUpCoupon(member.getId());
}
}
- 위의 로직은 회원가입을 하는 로직이다.
- 세가지 과정이 있다.
- 먼저 member 엔티티를 영속화한다.
- 그 다음 외부 시스템을 호출해서 이메일을 전송한다.
- 회원가입 쿠폰을 발급해준다.
- 위의 세가지 로직 모두 @Transactional을 가지고 있는 메소드이다.
- 만약 쿠폰을 발급하는 과정에서 예외가 터진다면?
- @Transational 기본 Propagation 속성이 Required 이기 때문에 부모 @Transactional 에 묶인 하위 트랜잭션은 모두 롤백된다.
- 그렇기 때문에 이메일을 발급하고 member 엔티티를 영속화하는 작업도 롤백되길 원한다.
- 하지만, 이메일을 보내는 작업은 외부 시스템이기 때문에 로백이 되지 않는다.
- 이러한 문제를 아래와 같이 해결할 수 있다.
@Getter
@RequiredArgsConstructor
public class MemberSignedUpEvent {
private final Member member;
}
public class MemberSignUpService {
private final MemberRepository memberRepository;
private final CouponIssueService couponIssueService;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void signUp(final MemberSignUpResquest dto) {
final Member member = memberRepository.save(dto.toEntity());
eventPublisher.publishEvent(new MemberSignedUpEvent(member));
couponIssueService.issueSignUpCoupon(member.getId());
}
}
- 이메일을 보내는 작업을 eventPublisher 를 통해서 이벤트로 발행한다.
@Component
@RequiredAgrsConstructor
public class MemberEventHander {
private final EmailSenderService emailSenderService;
@TransactionalEventListener
public void memberSignedUpEventListner(MemberSignedUpEvent dto) {
emailSenderService.sendSignUpEmail(dto.getMember());
}
}
- @TransactionalEventListener 는 @EventListener와는 다르게 commit이 되어야 실행된다.
- 결국 실행 순서가 영속화 -> 쿠폰발급 -> 이메일 전송이 된다.
- 따라서 같은 member 객체가 등록되고 쿠폰이 발급되어야지만 이메일이 전송된다.
REFERENCES