모르지 않다는 것은 아는것과 다르다.

Spring

ApplicationEventPublisher

채마스 2022. 6. 11. 11:29

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

'Spring' 카테고리의 다른 글

AbstractRoutingDataSource를 통한 Multi-DataSource 구현  (0) 2023.02.04
JDK 동적 프록시  (0) 2022.08.06
MapStruct  (0) 2022.04.09
Handler Methods  (0) 2022.04.09
함수 기반 API 설계  (0) 2022.03.19