작성
·
269
0
@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
public void createUser(CreateUserRequest request) {
Users users = firebaseUsersRepository.findUsersByFirebaseUid(request.getFirebaseUid())
.orElseThrow(() -> new BusinessException("Not Found User", HttpStatus.INTERNAL_SERVER_ERROR));
User user = User.builder()
.name(users.getDisplay_name())
.firebaseUid(request.getFirebaseUid())
.build();
userRepository.save(user);
}
}
@Component
@RequiredArgsConstructor
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class BaseEntityAuditAware implements AuditorAware<User> {
private final UserRepository userRepository;
@Override
public Optional<User> getCurrentAuditor() {
try {
return userRepository.findById(ApiLogger.getRequestCallerId());
} catch (Exception e) {
return Optional.empty();
}
}
}
createUser에서 userRepository.save(user)를 호출할때,
JpaAudit기능을 이용하기 위해 구현해놓은 BaseEntityAuditAware에서 유저정보를 가져온 후,
실제 쿼리를 날립니다.
이때, 전파속성이 REQUIRE_NEW이며, 발생한 모든 예외를 catch했으므로
이 함수를 호출한 부모 함수로 해당 예외가 전달되지 않을 것이기때문에 rollback이 되지 않으리라 기대했지만
실제로는 unexpectedrollbackexception이 발생하며 롤백이 되었습니다.
null을 반환하는건 문제가 아닌것이,
실제로 예외를 발생시키지 않으려고 위 코드를 아래와같이 변경하였더니 null값으로 정상적으로 insert쿼리가 날라갔습니다.
@Override
public Optional<User> getCurrentAuditor() {
Long callerId = ApiLogger.getRequestCallerId();
if (callerId == null)
return Optional.empty();
return userRepository.findById(ApiLogger.getRequestCallerId());
}
어느부분이 잘못된것이며 제가 오개념을 잡고있는 부분이 어디일까요?
답변 1
0
여러가지 테스트 결과,
아래와 같은 흐름으로 테스트 코드를 구성해보면
A(REQUIRES) --> 호출 --> B(REQEUIRES_NEW) --> 호출 --> C(REQUIRES)
위와같은 호출 순서와, 전파속성을 가지고있을때,
B에서 발생한 Unchecked Exception에 대해해서는
B에서 try-catch 해준다면 롤백마킹이 되지 않습니다.
하지만 C에서 발생한 Unchecke Exception에 대해서는
똑같이 롤백마킹이 됩니다.
그런데 여기서 의아한점은,
TransactionSynchronizationManager.getCurrentTransactionName()을 찍어보면
C의 트랜잭션이름이 B에서 새로생성된 트랜잭션 이름이라는점입니다.
따라서 getCurrtentAuidtor에서는 try-catch를 해주었더라고,
getCurretnAuditor에서 호출하는 findById가 UncheckedExcpetion을 발생시켰기때문에
createUser에서도 롤백이 발생한거 같습니다,
안녕하세요. 조성락님
블로그에 정리해두신 글을 보니 어떤 문제로 고민하시는지 이해가 되네요 🙂
다음 부분을 차근차근 공부해보시면 정확히 어떤 이유로 이런 문제가 발생하는지 정답을 찾을 수 있을거에요. 핵심은 UnexpectedRollbackException이 왜 발생하는지 정확히 이해를 하셔야 합니다.
스프링 DB 2편
10. 스프링 트랜잭션 전파1 - 기본 -> 스프링 트랜잭션 전파6 - 내부 롤백
11. 스프링 트랜잭션 전파2 - 활용 -> 트랜잭션 전파 활용6 - 복구 REQUIRED
감사합니다.
안녕하세요
알려주신
스프링 트랜잭션 전파6 - 내부 롤백
트랜잭션 전파 활용6 - 복구 REQUIRED
에서는 서로 같은 트랜잭션안에 있을때, 논리 트랜잭션중 어느 하나라도 예외가 터지면
롤백마킹이 되어 물리 트랜잭션이 롤백이 된다는 내용인데요,
스프링 트랜잭션 전파7 - REQUIRES_NEW
트랜잭션 전파 활용7 - 복구 REQUIRES_NEW
의 경우, REQUIRES_NEW인 논리 트랜잭션이 롤백이 되더라도,
트랜잭션이 분리되어있기때문에, 이를 호출한 다른 물리트랜잭션은 커밋이 된다는 내용으로 이해했습니다.
그래서 저의 상황인
A -> B -> C
A(REQUIRED)에서 B(REQUIRES_NEW)를 호출하고
B(REQUIRES_NEW)가 C(REQUIRED)를 호출한 상황에서
C에서 RuntimeExeption이 발생하여, C,B가 롤백이 되었어도,
트랜잭션1 : A
트랜잭션2 : B,C이므로
롤백마킹이 A까지 전파되지 않아야하는것이 아닌가? 하는 의문이 해결되지 않습니다.
왜냐하면
A와 C는 다른 트랜잭션이고,
C에서 발생한 exception은 B에서 try-catch했기때문에 A까지 전달되지 않았기 때문입니다.
트랜잭션 전파 활용7 - 복구 REQUIRES_NEW
트랜잭션 전파 활용7 - 복구 REQUIRES_NEW
혹시 제가 이해한것이 맞을까요?
디버깅을 통해 확인한 결과
1. C에서 발생한 RuntimeException을 B에서 TRY-CATCH했고
C에서 롤백마킹을 햇으며, B에서 커밋을 시도할때, 롤백마킹이 되어있어
UnexpectedRollbackException을 발생시킴.
2. 이예외는 B가 invoke된 후 Transaction AOP에서 호출한것이기때문에 A까지 전달되됨. A에서는 UnexpectedRollBackException이라는 RuntimeException이 발생하여 rollback 됨
-----
제가 이해한것이 맞다면
왜 A(REQUIRED) --> B(REQUIRES_NEW) --> C(REQUIRES_NEW)에서는
UnexpectedRollbackException이 A까지 전달되지 않은것일까요?
A(REQUIRED) --> B(REQUIRES_NEW) --> C(REQUIRES_NEW)
디버깅 결과
이상황에서는
C에서 발생한 Runtime Exception이 발생 , C 트랜잭션의 롤백마킹
2. B에서 예외를 캐치한 후, 마킹되어있는 롤백을 확인했더니
status.isGlobalRollbackOnly();가 false
그래서 정상 커밋됨.
3. 그래서 UnexpectedRollbackException이 발생하지않아 A는 정상커밋
안녕하세요. 조성락님
우선 질문하신 내용은 다음과 같았습니다.
A(REQUIRED)
B(REQUIRES_NEW) -> C(REQUIRED)
결과적으로 A와, B 2개의 물리 트랜잭션이 존재하는 것이지요. C의 경우에는 B에 포함된 논리적인 트랜잭션입니다.
이 경우 C에서 롤백 마킹을 하게 됩니다. 그러면 B에서 커밋을 하는 순간에 UnexpectedRollbackException이 발생하게 됩니다.
그런데 이 예외가 AOP에서 발생하기 때문에 [A] -> [B의 AOP] -> [B]와 같이 됩니다.
결과적으로 [B의 AOP]에서 UnexpectedRollbackException을 던집니다.
A가 이 예외를 명확하게 잡아서 예외를 제거하면 A를 정상 커밋할 수 있습니다.
A가 이 예외를 잡지 않고 그냥 둔다면 A도 예외가 발생했기 때문에 트랜잭션이 롤백됩니다.
감사합니다.
위 수정전 코드에서
ApiLogger.getRequestCallerId()를 예외처리 해주지않아
findById자체에서 NULL이 들어가
org.springframework.dao.InvalidDataAccessApiUsageException
이 발생했고,
findById도 @Transactional이기때문에
getCurrentAuditor
에서 롤백마킹이 된거같습니다.(추측)
그런데 궁금한점은 REQUIRES_NEW이기때문에, 여기서 롤백이 끝나야 할꺼같지만, 상위까지 롤백이 전파되었다는 점입니다.