[트러블슈팅] ILikeYou 프로젝트 - 스프링 이벤트(pub/sub) 처리에서 트랜잭션 관리

2023. 8. 2. 19:42트러블슈팅

https://developer-joon.tistory.com/162  스프링 이벤트 도입에 대한 글입니다.

 

[리팩토링] ILikeYou 프로젝트 스프링 이벤트, pub/sub구조 적용 (의존성 줄이기)

기능을 추가하다 보니, service 에서 다른 service와 결합도가 너무 높아졌다. 그리고 서비스 계층이 수행하는 일이 너무 많아져서 코드가 복잡해져서 가독성도 좋지 않았다. 그래서 코드를 기능(행

developer-joon.tistory.com

[문제발생 1]

ILikeYou 프로젝트에서 의존성을 낮추기 위해 스프링 이벤트를 적용하여서 코드를 분리했다.

하지만 이슈가 있음을 알 수 있었다.

@Transactional
public RsData<LikeablePerson> like(Member member, String username, int attractiveTypeCode) {
    RsData<LikeablePerson> canLikeResult = validator.validateLike(member, username, attractiveTypeCode);
    // S- : 성공 , F-0 : 수정가능, F- : 실패

    //수정 코드 받은 경우
    if (canLikeResult.getResultCode().equals("F-0")){
        return modifyAttractiveTypeCode(attractiveTypeCode, canLikeResult.getData());
    }

    if (canLikeResult.isFail())
        return canLikeResult;

    InstaMember fromInstaMember = member.getInstaMember();
    InstaMember toInstaMember = instaMemberService.findByUsernameOrCreate(username).getData();

    LikeablePerson likeablePerson = createLikeablePerson(attractiveTypeCode, fromInstaMember, toInstaMember);
    likeablePersonRepository.save(likeablePerson); // 저장
    applicationEventPublisher.publishEvent(new EventAddLike(this, likeablePerson));
    return RsData.of("S-1", "입력하신 인스타유저(%s)를 호감상대로 등록되었습니다.".formatted(username), likeablePerson);
}

코드를 보면 트랜잭션 내에서 save가 발생한 후 이벤트를 발생시킨다.

 

만약 트랜잭션이 비정상적으로 종료되어 롤백되었음에 불구하고,

이벤트를 구독해서 처리되는 부가적인 기능들은 별도로 처리되어 커밋되는 상황이 발생할 수도 있다.

 

[해결 1]

그래서 찾아보니, 스프링에서는 EventListener뿐 아니라 TransactionalEventListener을 제공한다.

TransactionalEventListener을 들어가보면 다음과 같은 내용을 볼 수 있다.

public @interface TransactionalEventListener {

   /**
    * Phase to bind the handling of an event to.
    * <p>The default phase is {@link TransactionPhase#AFTER_COMMIT}.
    * <p>If no transaction is in progress, the event is not processed at
    * all unless {@link #fallbackExecution} has been enabled explicitly.
    */
   TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;

 

TransactionPhase 까지 타고 들어가면 phase는 4가지의 단계를 볼 수 있다.

그리고 default값은 AFTER_COMMIT이다.  즉, 커밋된 후에 이벤트를 발행한다는 것이다.

  • AFTER_COMMIT (트랜잭션이 성공했을 때 실행)
  • AFTER_ROLLBACK (트랜잭션 롤백시 실행)
  • AFTER_COMPLETE 트랜잭션 완료시 (AFTER_COMMIT+AFTER_ROLLBACK)
  • BEFORE_COMMIT (트랜잭션 commit 되기전에)

따라서 default로 냅두면 모든 것이 완벽할 것이라고 생각이 들었다.

[문제발생 2]

하지만 실제 디버깅 해보니,

mainProcess가 성공적으로 commit이 완료되었고, doAfterMainProcessCommit() 메소드가 실행되는 걸 디버깅을 통해 확인할 수 있었지만 실제 커밋 이후에 발생해야 하는 쿼리가 나가질 않았다.

 

[해결 2]

 

그래서 AFTER_COMMIT 에서 link가 걸려있는 TransactionSynchronization.afterCommit()에 대한 설명을 더 살펴보았다.

Use PROPAGATION_REQUIRES_NEW for any transactional operation that is called from here.

 afterCommit에서 호출되는 작업에는 PROPAGATION_REQUIRES_NEW 을 사용하라고 한다.

즉, TransactionalEventListener에서 commit이 완료되었고 이후의 추가적인 변경 작업에 대해서는 commit이 발생하지 않기 때문에  afterCommit에서 트랜잭션 작업에는 PROPAGATION_REQUIRES_NEW 을 사용해야 한다.

 

최종적으로 EventListener은 아래와 같은 구조를 가지게 되었다.

@Component
@RequiredArgsConstructor
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class NotificationEventListener {

    private final NotificationService notificationService;

    @TransactionalEventListener
    public void listen(EventAddLike event) {
        notificationService.afterAddLike(event.getLikeablePerson());
    }
    @TransactionalEventListener
    public void listen(EventModifiedAttractiveType event) {
        notificationService.afterModifyLike(event.getLikeablePerson(), event.getOldAttractiveTypeCode(), event.getNewAttractiveTypeCode());
    }
}