일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- JPA
- 미션
- mock
- 레벨2
- JUnit5
- 코드리뷰
- HTTP
- AWS
- 트랜잭션
- 우아한테크코스
- Docker
- Spring Batch
- CircuitBreaker
- MSA
- 세션
- yml
- 의존성
- 서블릿
- AOP
- Level2
- 프리코스
- REDIS
- 스프링부트
- 우테코
- 스프링 부트
- 우아한세미나
- Paging
- 자바
- 프로그래머스
- 백준
- Today
- Total
늘
[Transaction] commit된 트랜잭션에 롤백된 트랜잭션이 참여하면 어떻게 될까? TranscationalEventListener와 함께 알아보자 본문
[Transaction] commit된 트랜잭션에 롤백된 트랜잭션이 참여하면 어떻게 될까? TranscationalEventListener와 함께 알아보자
giron 2022. 12. 2. 02:30스프링의 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
만약 트랜잭션이 이미 종료되었으면 예외를 발생한다. 즉, 이벤트는 커밋이 되었어도 트랜잭션이 종료되어있지 않다는 말은 사실이었다.
#AbstractPlatformTransactionManager.java 이어서
# TransactionImpl
트랜잭션이 살아있는 상태인지 계속해서 체크하는 것을 알 수 있습니다.
#AbstractPlatformTransactionManager.java 이어서2
최종적으로 커밋을 합니다. 흐름을 보면 Active 한 상태인지 확인을 계속하면서 롤백 마크가 있는지 확인을 합니다. 만약 롤백 마크도 없고 active 한 상태라면 최종적으로 commit을 진행하고 COMMIT마크를 남깁니다. 이미 커밋이 되었고 커밋 마크도 남겼으니 예외가 터졌다고 해도 변경에 영향이 없던 것입니다. 생각해보면 당연한 부분이었군요.🤷♂️
마치며
이벤트를 처음 적용해보면서 재밌는 사실들을 많이 배웠습니다. 트랜잭션이 커밋이 되거나 롤백이 되어도 트랜잭션은 종료되지 않고 active 하고 accessible 할 수 있다는 것을 알았습니다.(생각해보니 직접 트랜잭션 구현할 때도, commit하고 close했던 것을 생각해보면 당연히 commit과 트랜잭션 종료는 별개네요..) 하지만 이미 커밋 or 롤백이 되었기 때문에 새로운 변경을 커밋할 수는 없습니다.
따라서 @TransactionalEventListener을 사용할때는 try-catch를 안 하고도 새로운 트랜잭션에서 발생하는 예외에 영향을 받지 않을 수 있습니다.
트랜잭션을 타고 들어가다 보니 commit이 되어도 해당 메서드를 지나간다. global rollback-only가 적용되면 로컬에서 커밋이 일어나도 롤백이 발생하는 것 같다. 추후에 알아보겠습니다.
Reference
https://www.baeldung.com/spring-events#transaction-bound-events
'백앤드 개발일지 > 스프링부트' 카테고리의 다른 글
[프록시] 스프링에서 사용되는 proxy전략 (2) | 2022.07.23 |
---|---|
[RestDocs]API 문서화 (2) | 2022.06.03 |
@Mock vs @MockBean vs @InjectMocks (1) | 2022.05.31 |
[Spring bean lifecycle, hook]빈 생명주기 (0) | 2022.05.23 |
@RestController와 @ResponseBody없이 json으로 통신하는 방법 (2) | 2022.04.30 |