JAVA/Spring

Scheduling 시 Exception 발생 처리 문제

laughcryrepeat 2023. 7. 17. 15:24

 1분마다 DB 조회해서 스케줄링으로 처리하는 로직이 있는데, 프로세스 중 Runtime Exception 이 발생하였습니다.

그런데 그 처리 로직 메소드에는 @Transactional 이 걸려있어서 예외 발생시에는 rollback 이 되고 있었는데요. 

때문에 1분마다 도는 스케줄링에 계속해서 같은 예외에 걸려서 다른 프로세스로 넘어가지를 못하고 계속해서 에러를 내뿜고 있었습니다.

 

org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only

 

 

스케줄러의 로직은 다음과 같습니다.

@Component
@RequiredArgsConstructor
@Slf4j
public class SessionEndScheduler {

  private final SessionProcessingService sessionProcessingService;
  
  /**
     * 세션 종료 처리.
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @Scheduled(fixedRate = 60000, initialDelay = 3000) // 초기 지연시간 3초, 작업 수행 시작 주기 60초
    public void endCandidateSessionStateWhenTestDurationFinished() {
        log.info("endSessionState scheduler started");
        sessionProcessingService.handleSessionEndForAll();
    }

}

 

SessionProcessingService는 종료 처리 하려는 Session을 조회하고, 조회한 세션을 순차 종료처리 합니다.

이때 호출되는 endPart 서비스 메소드에도 @Transactional 이 걸려 있습니다. 

 

@Service
@RequiredArgsConstructor
@Slf4j
public class SessionProcessingService {

  private final CandidateService candidateService;
  
      /**
     * 스케줄러에서 호출.
     * 종료시간에 의해 자동 종료 처리
     *
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @Transactional
    public void handleSessionEndForAll() {
        // 스케줄러로 호출
        List<CandidateSession> candidateSessions = candidateSessionRepository.getNonEndedSession();
        log.info("finished candidateSession total: {}",candidateSessions.stream().map(x -> x.getId().toString()).collect(Collectors.joining(",")));
        for (CandidateSession candidateSession : candidateSessions) {
            // 세션 순차 종료 처리.
            try {
                log.info("sessionId: {}", candidateSession.getId());
                candidateService.endPart(candidateSession);
                candidateSession.updateEndStatus(EndStatus.AUTO);
                candidateSessionRepository.update(candidateSession);
            } catch (Throwable t) {
            // 이때 에러가 발생할 경우 end fail처리하도록 catch 해준다.
                log.info("Error during candidate session end processing: {}", t.getMessage());
                candidateSession.setEndFail();
                candidateSessionRepository.update(candidateSession);
            }
        }
    }
}

 

 그런데 세션을 종료 처리하는 곳에서 예외가 발생하게 되었을때 Transactional 에 의해서 롤백 처리되면서, 주기적으로 도는 스케줄러에서는 계속해서 Exception이 발생하게 되었습니다. 

 그래서 메서드를 try catch 문으로도 잡아서 데이터를 fail 상태로 변경하여 제외하려고 했는데요. 

 계속해서 예외가 발생하면서 롤백되고 스케줄링 프로세스가 제대로 동작하지 않았습니다. 선언적 트랜잭션 @Transactional 에서 잡은 Exception이면 당연히 catch 문 안으로 떨어져야 한다고 생각했습니다.

 

 왜 이런 현상이 발생한 것일까요?

이것을 이해하려면 Exception 종류, @Transactional 내부에서 어떤 Exception 발생시 롤백하는지와 롤백 시 트랜잭션의 동작 원리를 살펴봐야 합니다.

 

Throwable 구조 및 Exception에 대한 이해

 

 

Checked Exception

  • Exception 클래스를 직접 상속받습니다.
  • 반드시 try/catch 블록 으로 잡거나, throws 로 던져져야 합니다.
  • 적절한 Exception handling 하지 않으면 컴파일 에러가 발생합니다.
  • 예외 발생시 롤백 하지 않습니다.

 

Unchecked Exception

  • RuntimeException 을 상속받습니다.
  • 컴파일 타임에 확인되지 않아 'unchecked' 라고 이름붙여 졌습니다.
  • try/catch 블럭으로 잡을 수 있습니다.
  • 대부분 프로그래밍 에러입니다.
  • 예외 발생시 롤백해야 합니다.

 

Exception 과 Transaction Rollback 

@Transactional 의 propagation 기본 속성은 Propagation.REQUIRED 입니다. 트랜잭션이 없으면 생성되고 있으면 기존의 트랜잭션에 따라갑니다. 따라서 스케줄러에서 호출한 `handleSessionEndForAll` 메소드의 트랜잭션에 참여해 `candidateService.endPart` 메소드도 같은 트랜잭션 내에 있게 됩니다.

 @Transactional 에서 Exception 이 발생하면 rollback 마크를 하게 되고 해당 트랜잭션은 쓸수 없게 되는데요. 호출한 메서드내에서 Exception이 발생하고 외부 메서드에서 try/catch문으로 잡았다고 하더라도, 같은 트랜잭션에 있기 때문에 rollback이 진행됩니다. 

 

해결

그렇다면 초기에 호출된 메서드의 트랜잭션과 두번째 호출한 메서드의 트랜잭션을 분리하면 해결이 가능해 보였습니다.

그래서 Transaction의 propagation 을 Popagation.REQUIRES_NEW 로 설정해주었더니 메서드 외부에서 Exception 잡아서 session 업데이트 처리가 가능했습니다.

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void endPart(final CandidateSession candidateSession) {
	...
}

 

 

Reference

https://techblog.woowahan.com/2606/

 

응? 이게 왜 롤백되는거지? | 우아한형제들 기술블로그

{{item.name}} 이 글은 얼마 전 에러로그 하나에 대한 호기심과 의문으로 시작해서 스프링의 트랜잭션 내에서 예외가 어떻게 처리되는지를 이해하기 위해 삽질을 해본 경험을 토대로 쓰여졌습니다.

techblog.woowahan.com