- 넘 오랜만의 기록이다 24년이 되어버렸어
시간 여유 되면 23년 회고도 써봐야쥐- 요즘 회사 코드 통합작업이랑 리팩토링하면서 기존코드 파악하고 머리쓰느라 정신없당
서비스가 커지면서 슬슬 순환참조 문제도 생기고,,
순환참조 없애볼겸 리팩토링하면서 아키텍쳐 바꿔보자구 스터디하면서 소소하게 ddd 적용해보는중인데 생각보다 쉽지 않다 서로의 의견도 다 달라서 논의하다보면 가끔 회의실이 핫하다 - 아무튼 그러던 와중에 맞이한 에러
Transaction silently rolled back because it has been marked as rollback-only
분명 예외처리를 해주었는데도 롤백되길래 관련사항들을 파악해보았다
- 요즘 회사 코드 통합작업이랑 리팩토링하면서 기존코드 파악하고 머리쓰느라 정신없당
이슈 내용
에러를 살펴보니 채팅 서버에서 응답을 못받은 경우 던지는 예외와 관련이 있었다
그런데 이 예외는 메서드를 호출한 메서드에서 잡아서(try-catch) 기본값을 대신 응답하도록 되어있었다
Spring의 선언적 트랜잭션(@Transactional
) 안에서 예외를 잡았기 때문에 당연히 롤백 없이 커밋될거라 예상했는데 에러가 발생했다
회사 코드를 쓸 수 없으니 비슷한 상황을 만들어보자면
- A 클래스 내에 있는, 메서드를 호출한 메서드
@Transactional
public Test test(String a) {
...
try {
b.inner();
return new Test(a);
} catch (TestException e) {
// 예외 발생한 경우 기본값 응답하도록
return new Test();
}
}
- B 클래스 내에 있는, 예외를 던진 메서드
@Transactional
public Inner inner() {
...
throw new TestException();
}
기대했던 동작 & 이와 다른 부분
-
B.java의 inner()에서
throw new TestException();
를 통해 예외를 던짐 -
해당 메서드를 호출한 A.java의 test()에서 try catch를 통해 예외 처리함
-
💡 예상했던 방향
현재 선언적 트랜잭션(@Transactional)에서 별도로 전파 속성을 주지 않았기 때문에 default인PROPAGATION_REQUIRED
니까
→B.java의 inner()
는 호출한 쪽인A.java의 test()
의 이미 만들어진 트랜잭션에 참여하게 될 것이다
→ 따라서 호출한 쪽에서 try catch를 통해 예외처리를 할 수 있을 것이다⇒ 디버깅을 통해 원하는대로 catch되어 요기까지 오는걸 확인함
=> 그러나 끝까지 처리되지 않고(=return 하지 않고) UnexpectedRollbackException
발생
Why?
(도대체 내가 어디에서 rollback-only를 마킹했다는거야..!)
디버깅으로 따라가다 만난 코드를 보면
txInfo.transactionAttribute.rollbackOn(ex)
따라가보면
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
따라가다 보면
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
// Participating in larger transaction
if (status.hasTransaction()) {
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
doSetRollbackOnly(status); //요기서 롤백이 마킹되었음
}
...
// Raise UnexpectedRollbackException if we had a global rollback-only marker
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
//익숙한 예외메시지~,,
"Transaction rolled back because it has been marked as rollback-only");
}
}
...
}
- 던진 예외인 TestException은 RuntimeException이기 때문에 rollback-only 마킹을 하게 됨
- 참여 중인 트랜잭션이 실패하면 기본정책이 전역롤백(= globalRollbackOnParticipationFailure default 값이 true) 이기 때문에 마지막 순간에 UnexpectedRollbackException을 던짐
globalRollbackOnParticipationFailure 속성 (주석)
Set whether to globally mark an existing transaction as rollback-only after a participating transaction failed.
Default is “true”: If a participating transaction (e.g. with PROPAGATION_REQUIRED or PROPAGATION_SUPPORTS encountering an existing transaction) fails, the transaction will be globally marked as rollback-only. The only possible outcome of such a transaction is a rollback: The transaction originator cannot make the transaction commit anymore.
참여 중인 트랜잭션이 실패한 후에 기존 트랜잭션을 전역적으로 rollback-only로 마킹할 것인지 설정
디폴트는 true
PROPAGATION_REQUIRED 또는 PROPAGATION_SUPPORTS 인 참여 중인 트랜잭션이 실패하면, 그 트랜잭션은 전역적으로 rollback-only로 마킹됨
이런 트랜잭션은 결과적으로 롤백되고 최초의 트랜잭션관리자도 그 트랜잭션을 커밋시킬 수 없음
한마디로 정리해보면,
전파속성때문에 실제 트랜잭션이 재사용되어서 커밋이나 롤백같은 최종완료처리는 최초 트랜잭션이 반환될 때 일어나더라도, 트랜잭션의 완료처리(completion)는 트랜잭션 메서드의 반환시점마다 하기 때문에 마킹만 해두고 최초의 트랜잭션이 완료처리되는 마지막 순간에 UnexpectedRollbackException을 던진것!
따라서 @Transactional 에서 예외가 터지면 롤백 마크를 하기 때문에 해당 트랜잭션을 재사용할 수 없다
해결
- globalRollbackOnParticipationFailure 속성을 false로 바꾼다
⇒ 전체 트랜잭션에서 다 바뀌므로 굳이! - 현재 채팅 서버가 응답을 주지 않을 경우 별도의 정책이 있기 때문에
채팅서버와의 통신에서 실패 시, 별도의 예외를 던지지 않고 정책에 맞는 값을 return 한다 ✔️
Reference
우아한 기술블로그 : 응? 이게 왜 롤백되는거지?
Transaction silently rolled back because it has been marked as rollback-only
당신은 트랜잭션에 대해 얼마나 알고 있는가