인프런 커뮤니티 질문&답변

조성락님의 프로필 이미지

작성한 질문수

스프링 DB 2편 - 데이터 접근 활용 기술

REQUIRES_NEW인데 rollback되는 이유가 궁금합니다.

24.08.02 18:52 작성

·

229

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

김영한님의 프로필 이미지
김영한
지식공유자

2024. 08. 04. 17:31

안녕하세요. 조성락님

이 문제는 저도 잘 모르겠습니다.

관련해서 비슷한 경험을 하신 분 있으시면 답변 부탁드려요.

감사합니다.

조성락님의 프로필 이미지
조성락
질문자

2024. 08. 05. 10:22

@Override public Optional<User> getCurrentAuditor() { 
try { 
   return userRepository
      .findById(ApiLogger.getRequestCallerId()); 
  } catch (Exception e) { 
   return Optional.empty(); 
  } 
}


위 수정전 코드에서
ApiLogger.getRequestCallerId()를 예외처리 해주지않아

findById자체에서 NULL이 들어가
org.springframework.dao.InvalidDataAccessApiUsageException
이 발생했고,

findById도 @Transactional이기때문에getCurrentAuditor
에서 롤백마킹이 된거같습니다.(추측)

그런데 궁금한점은 REQUIRES_NEW이기때문에, 여기서 롤백이 끝나야 할꺼같지만, 상위까지 롤백이 전파되었다는 점입니다.



조성락님의 프로필 이미지
조성락
질문자

2024. 08. 05. 11:07

여러가지 테스트 결과,
아래와 같은 흐름으로 테스트 코드를 구성해보면
A(REQUIRES) --> 호출 --> B(REQEUIRES_NEW) --> 호출 --> C(REQUIRES)
위와같은 호출 순서와, 전파속성을 가지고있을때,

B에서 발생한 Unchecked Exception에 대해해서는
B에서 try-catch 해준다면 롤백마킹이 되지 않습니다.

하지만 C에서 발생한 Unchecke Exception에 대해서는
똑같이 롤백마킹이 됩니다.

그런데 여기서 의아한점은,
TransactionSynchronizationManager.getCurrentTransactionName()을 찍어보면
C의 트랜잭션이름이 B에서 새로생성된 트랜잭션 이름이라는점입니다.

조성락님의 프로필 이미지
조성락
질문자

2024. 08. 05. 11:09

따라서 getCurrtentAuidtor에서는 try-catch를 해주었더라고,
getCurretnAuditor에서 호출하는 findById가 UncheckedExcpetion을 발생시켰기때문에
createUser에서도 롤백이 발생한거 같습니다,

조성락님의 프로필 이미지
조성락
질문자

2024. 08. 05. 13:21

김영한님의 프로필 이미지
김영한
지식공유자

2024. 08. 05. 21:38

안녕하세요. 조성락님

블로그에 정리해두신 글을 보니 어떤 문제로 고민하시는지 이해가 되네요 🙂

다음 부분을 차근차근 공부해보시면 정확히 어떤 이유로 이런 문제가 발생하는지 정답을 찾을 수 있을거에요. 핵심은 UnexpectedRollbackException이 왜 발생하는지 정확히 이해를 하셔야 합니다.

스프링 DB 2편

  • 10. 스프링 트랜잭션 전파1 - 기본 -> 스프링 트랜잭션 전파6 - 내부 롤백

  • 11. 스프링 트랜잭션 전파2 - 활용 -> 트랜잭션 전파 활용6 - 복구 REQUIRED

     

감사합니다.

조성락님의 프로필 이미지
조성락
질문자

2024. 08. 06. 09:58

관심가지고 읽어주셔서 감사합니다 다시한번 차근차근공부해보겠습니다!

조성락님의 프로필 이미지
조성락
질문자

2024. 08. 06. 10:24

안녕하세요
알려주신
스프링 트랜잭션 전파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

조성락님의 프로필 이미지
조성락
질문자

2024. 08. 06. 10:28

혹시 제가 이해한것이 맞을까요?
디버깅을 통해 확인한 결과


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까지 전달되지 않은것일까요?

조성락님의 프로필 이미지
조성락
질문자

2024. 08. 06. 10:43

A(REQUIRED) --> B(REQUIRES_NEW) --> C(REQUIRES_NEW)
디버깅 결과

이상황에서는

  1. C에서 발생한 Runtime Exception이 발생 , C 트랜잭션의 롤백마킹
    2. B에서 예외를 캐치한 후, 마킹되어있는 롤백을 확인했더니
    status.isGlobalRollbackOnly();가 false
    그래서 정상 커밋됨.
    3. 그래서 UnexpectedRollbackException이 발생하지않아 A는 정상커밋

     

김영한님의 프로필 이미지
김영한
지식공유자

2024. 08. 07. 20:16

안녕하세요. 조성락님

우선 질문하신 내용은 다음과 같았습니다.

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도 예외가 발생했기 때문에 트랜잭션이 롤백됩니다.

감사합니다.

조성락님의 프로필 이미지
조성락
질문자

2024. 08. 08. 10:07

제가 이해한게 맞았네요 감사합니다!!!