본문 바로가기
스프링 부트

[스프링부트] Event 발행과 @EventListener에 대해 알아보자

by illlilillil 2022. 7. 6.

이벤트란?

이벤트 발생이란 상태가 변경됨을 의미한다. 상태가 변경되고 해당 이벤트에 해당하는 동작을 수행하는 기능을 구현할 수 있다.

 

이벤트 생성 과정

  1. 이벤트 생성주체가 이벤트 생성
  2. 퍼블리셔가 이벤트 발행
  3. 이벤트 핸들러(구독자)

이벤트의 용도

  1. 트리거: 도메인 상태가 바뀔때마다 후처리가 필요할때 사용
  2. 다른 도메인 간의 동기화: 주문 도메인에서 배송지 변경 이벤트를 발생 시키고 구독자인 핸들러는 외부 배송 서비스와 이를 동기화하는 로직을 구성할 수 있다.

이벤트의 장점

  1. 연관 관계 제거 - 서로 다른 도메인 간의 의존 관계를 제거할 수 있다.
  2. 기능 확장 용이 - 구매 취소 시 환불과 함께 이메일 전송이 필요하다면 이메일 발송 이벤트를 구현하면 된다.

 

프로젝트에 적용하기

에어비앤비같은 숙소 예약 플랫폼을 제작하는 도중 제가 맡은 리뷰에 대한 로직을 작성하고 있었다.

리뷰 작성 또는 수정 시 room 도메인의 정보 일부를 변경해야 해서 room에 대한 의존성에 의한 결합도가 증가함을 발견했다.

따라서 이벤트 처리를 통해 도메인 간 강결합 문제를 해소하기로 했다.

 

적용 방법

1. ApplicationEventPublisher를 주입받고 생성하고자 하는 이벤트를 발행한다.

private final ApplicationEventPublisher publisher;

----------------------------------------------------------------------------------------------------

publisher.publishEvent(
        new UpdateReviewInfoEvent(reservation.getRoomId(), createReviewRequest.getRating()));
       
----------------------------------------------------------------------------------------------------

@Getter
public class UpdateReviewInfoEvent {

  private final Long roomId;
  private final Integer rating;

  public UpdateReviewInfoEvent(Long roomId, Integer rating) {
    this.roomId = roomId;
    this.rating = rating;
  }
}

2. 리스너는 해당 이벤트를 구독하고 있다가 이벤트가 발행되면 로직을 수행한다.

@Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class UpdateReviewInfoListener {

  private final RoomRepository roomRepository;

  public UpdateReviewInfoListener(RoomRepository roomRepository) {
    this.roomRepository = roomRepository;
  }

  @Async
  @EventListener(UpdateReviewInfoEvent.class)
  public void handleContextStart(UpdateReviewInfoEvent event) {
    Room room = roomRepository.findById(event.getRoomId()).orElseThrow(() -> {
      throw new NotFoundException(this.getClass().getName());
    });
    room.getReviewInfo().updateReviewInfo(event.getRating());
  }
}

반드시 트랜잭션을 새로 만들어야 한다.

이미 커밋이 된 후이기 때문에 추가적인 데이터 조작이 있어도 1차 캐시에서는 반영되지 않는다. 그렇기에 새 트랜잭션을 구성해 데이터 조작을 해야 데이터가 잘 반영될 수 있다.

 

@EventListener는 동기식 이벤트 처리 방법이다. 만약 리뷰 작성에서 발행한 이벤트인 Room 업데이트 이벤트가 예외 처리된다면 롤백을 해야 하나에 대한 문제이다. 동기식 처리 방법은 이후 review의 로직에도 영향을 주게 된다. room에 대한 예외 때문에 review 작성이 불가하다는 점은 다시 한 번 생각해봐야 한다.

 

스프링 4.2버전 이후부턴 @TransactionalEventListener를 지원해 트랜잭션의 어떤 타이밍에 이벤트를 발생 시킬지에 대한 설정을 지원한다.

  • AFTER_COMMIT (기본값) - 트랜잭션이 성공적으로 마무리(commit)됐을 때 이벤트 실행
  • AFTER_ROLLBACK – 트랜잭션이 rollback 됐을 때 이벤트 실행
  • AFTER_COMPLETION – 트랜잭션이 마무리 됐을 때(commit or rollback) 이벤트 실행
  • BEFORE_COMMIT - 트랜잭션의 커밋 전에 이벤트 실행

 

나의 프로젝트에는 기본 값을 사용해도 무방할 거 같아 따로 설정은 하지 않았다.

바뀐 코드는 다음과 같다. 이벤트 생성 주체의 트랜잭션이 커밋되는 시점에 이벤트가 실행하도록 바뀌었고 @Async 어노테이션을 통해 비동기적으로 이벤트가 처리되도록 설정하였다.

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, classes = ChangeReviewInfoEvent.class)
public void handleContextStart(ChangeReviewInfoEvent event) {
  reviewInfoService.changeReviewInfo(event);
}

 

이러한 로컬 핸들러 방식은 이벤트를 발행하고 에러가 터졌을 경우 해당 이벤트가 정상 동작했다는 것에 대한 보장을 할 수 없다.

따라서 카프카라는 이벤트 브로커를 통해 에러가 터진 시점부터 다시 이벤트를 consume할 수 있어 장애에 좀 더 잘 대응할 수 있다.

또한 많은 양의 데이터를 처리할 수 있어 대용량 서비스에 적합하다.

댓글