[Transaction] commit된 트랜잭션에 롤백된 트랜잭션이 참여하면 어떻게 될까? TranscationalEventListener와 함께 알아보자 본문

백앤드 개발일지/스프링부트

[Transaction] commit된 트랜잭션에 롤백된 트랜잭션이 참여하면 어떻게 될까? TranscationalEventListener와 함께 알아보자

giron 2022. 12. 2. 02:30
728x90

스프링의 application context는 Application Event를 제공해준다. 해당 이벤트를 사용할 때 기존 트랜잭션에서 커밋이 된 후에 이벤트를 처리할 때, Transaction의 Propagation을 Requires_new 나 @Asnyc로 주지 않으면 같은 트랜잭션으로 묶여서 정상적으로 이벤트가 발급되지 않는다. 따라서 이벤트에 Propagation.REQUIRES_NEW 옵션을 주어서 사용했는데 이때 Requires_new를 사용한다고 해도 상위 트랜잭션에서 예외를 잡아주지 않으면 예외가 전파되어서 상위 트랜잭션도 롤백이 된다. 따라서 try-catch로 잡아줘야 한다. 

그런데.. 실제 event에 대해서 예외 테스트를 진행해줬는데 try-catch로 잡지 않아도 예외가 전파되지 않고 상위 트랜잭션은 커밋이 되었다. 어떻게 발생한 일인 걸까?

한 가지 추측으로는 TransactionEventListener는 트랜잭션이 커밋된 이후에 이벤트를 publish 해준다. 즉, 상위 트랜잭션에서 커밋 마킹이 되었고 이후 이벤트에서 발생하는 에러에 대해 콜 스택이 쌓이지만 커밋 마킹이 되었기 때문에 상위 트랜잭션에서 롤백 처리하지 않고 정상적으로 커밋을 처리한 것 같다.

즉, 예외가 발생했다고 무조건 롤백이 아니라 트랜잭션 커밋 마킹이 안 되어 있을 때, 예외가 발생해야 롤백이 된다.

한 번 실습을 해보면서 알아보자!

비즈니스 상황 가정

우리의 서비스가 Point를 사용하면 사용자에게 문자를 발급해주는 서비스라고 가정하자. 

#PointService

Point를 사용할 때 이벤트를 발생시키는 로직이다.

public class PointService {
	...
    
	public void use(PointRequest request) {
        
       	...
        
        eventPublisher.publishEvent(new SmsEvent(user.getName(), point.getAmount()));
    }
}

#SmsEventListener

Point를 사용할 때 발생한 이벤트를 받아 처리를 하는 메서드이다.

@Component
public class SmsEventListener {
	
    ...
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @TransactionEventListener
    public void send(SmsEvent event) {
    	smsService.send(event);
    }
}

#Test code

초기에 5천 포인트가 있고 100포인트를 사용하면 포인트는 정상적으로 차감이 되고 sms 기록에 저장이 되었는지 확인하는 테스트입니다.

public class PointTest {
	@Transactional
 	@Test
    void rollback() {
        pointRepository.save(new Point(5000L)); // 초기에 5천 포인트

        //when
        pointService.use(100L); // 100 포인트 사용
        Point point = pointRepository.findAll().get(0);
        TestTransaction.flagForCommit();
        TestTransaction.end();
        TestTransaction.start();

        List<Sms> smss = smsRepository.findAll();
        
        //then
        assertAll(
                () -> assertThat(point.getAmount()).isEqualTo(4900), // 5000- 100
                () -> assertThat(smss).hasSize(1)
        );
    }
}

실습

EventListener + propagation.REQUIRES_NEW 

만약 추측이 맞다면 트랜잭션이 커밋하기 전에 바로 이벤트를 publish 해주는 @EventListener를 사용하면 테스트에서 정상적으로 예외가 발생해야 한다.

이때 IllgealStateExcpetion을 발생시키면 상위 트랜잭션도 롤백이 되어야 한다.

#SmsEventListener 수정

@Component
public class SmsEventListener {
	
    ...
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @EventListener
    public void send(SmsEvent event) {
    	throw new IllegalStateException("REQUIRES_NEW 이벤트 예외 발생");
    }
}

예상대로 예외 발생

테스트를 돌려보면 추측했던 대로 예외가 발생했다. 콜 스택을 봐도 확인할 수 있었다.

예외 발생

그렇다면 처음 가설대로 트랜잭션에 커밋 마크가 먼저 붙으면 하위 트랜잭션에서 롤백이 일어나도 정상적으로 커밋이 된다는 것이다.

실제 다시 @TransactionEventListener를 적용해서 테스트를 해보면 아래처럼 2번째 트랜잭션만 롤백이 된다.

트랜잭션 로그

그렇다면 이번엔 코드를 통해서 알아보자. 어떤 구조이길래 이런 일이 발생할 까?

디버깅

깨끗하다.

#AbstractPlatformTransactionManager.java

status.iscompleted()일 때,

만약 트랜잭션이 이미 종료되었으면 예외를 발생한다. 즉, 이벤트는 커밋이 되었어도 트랜잭션이 종료되어있지 않다는 말은 사실이었다.

#AbstractPlatformTransactionManager.java 이어서

실제 커밋이 발생하는 로직이다.
자세히

# TransactionImpl

진짜찐짜 커밋
롤백 마크를 해도 isActive true일 수가 있다?!

트랜잭션이 살아있는 상태인지 계속해서 체크하는 것을 알 수 있습니다.

#AbstractPlatformTransactionManager.java 이어서2

진짜 진짜 진짜 커밋

최종적으로 커밋을 합니다. 흐름을 보면 Active 한 상태인지 확인을 계속하면서 롤백 마크가 있는지 확인을 합니다. 만약 롤백 마크도 없고 active 한 상태라면 최종적으로 commit을 진행하고 COMMIT마크를 남깁니다. 이미 커밋이 되었고 커밋 마크도 남겼으니 예외가 터졌다고 해도 변경에 영향이 없던 것입니다. 생각해보면 당연한 부분이었군요.🤷‍♂️

 

마치며

이벤트를 처음 적용해보면서 재밌는 사실들을 많이 배웠습니다. 트랜잭션이 커밋이 되거나 롤백이 되어도 트랜잭션은 종료되지 않고 active 하고 accessible 할 수 있다는 것을 알았습니다.(생각해보니 직접 트랜잭션 구현할 때도, commit하고 close했던 것을 생각해보면 당연히 commit과 트랜잭션 종료는 별개네요..) 하지만 이미 커밋 or 롤백이 되었기 때문에 새로운 변경을 커밋할 수는 없습니다. 

따라서 @TransactionalEventListener을 사용할때는 try-catch를 안 하고도 새로운 트랜잭션에서 발생하는 예외에 영향을 받지 않을 수 있습니다. 

추가 학습

트랜잭션을 타고 들어가다 보니 commit이 되어도 해당 메서드를 지나간다. global rollback-only가 적용되면 로컬에서 커밋이 일어나도 롤백이 발생하는 것 같다. 추후에 알아보겠습니다.

Reference

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html

https://www.baeldung.com/spring-events#transaction-bound-events

728x90
Comments