Kubernetes 환경에서 Spring Boot 배치 오류 전파 및 종료 코드 처리

1. 문제 상황

Spring Boot 기반 배치 애플리케이션을 실행하는 과정에서 오류 발생 시 CallRunner까지 예외 전파 및 종료 코드 처리가 제대로 이루어지지 않는 문제가 발생했다.

  • 예외 발생 시 JVM이 정상적으로 종료되지 않거나 무한 대기 상태에 빠지는 문제
  • System.exit() 호출 후에도 종료되지 않고 addShutdownHook만 실행되는 현상
  • Kubernetes 환경에서 Pod가 오류를 감지하지 못하거나, 불필요한 재시작이 반복되는 문제

2. 원인 분석

  • CallRunner에서 종료 처리를 수행하기 때문에 System.exit()이 중복 실행되면 무한 대기 발생
  • Shutdown Hook 내에서 System.exit()을 실행하면 JVM이 종료되지 않고 대기 상태에 빠지는 문제
  • 예외가 CallRunner까지 정상적으로 전달되지 않으면, 종료 코드가 제대로 설정되지 않음
  • Spring Boot의 SpringApplication.exit()이 실행될 때, 커스텀 종료 코드 설정이 반영되지 않을 수 있음

3. 해결 방법

✅ 1️⃣ 예외가 CallRunner까지 정상적으로 전파되도록 설정

  • throw e;을 명확히 사용하여 CallRunner에서 예외를 감지할 수 있도록 함.

✅ 2️⃣ System.exit() 실행 위치 조정

  • Shutdown Hook 내에서 System.exit()을 호출하지 않음.
  • CallRunner 실행 후 JVM 종료 코드만 제대로 반영되도록 처리.

✅ 3️⃣ Kubernetes Pod 종료 상태 처리 (exitCode 설정)

  • exitCode = 0 → 정상 종료 (Completed)
  • exitCode ≠ 0 → 비정상 종료 (Error), CrashLoopBackOff 가능성 있음
  • restartPolicy: Never 또는 restartPolicy: OnFailure로 조정

✅ 4️⃣ CustomExitCodeGenerator 활용

  • Spring Boot의 ExitCodeGenerator를 활용하여 종료 코드 설정을 개선
  • SpringApplication.exit(applicationContext, new CustomExitCodeGenerator(exitCode));

4. 최종 코드

public static void main(String[] args) {
AtomicInteger exitCode = new AtomicInteger(0);
AtomicReference<ConfigurableApplicationContext> contextRef = new AtomicReference<>(null);
AtomicBoolean shutdownHookExecuted = new AtomicBoolean(false);

try {
SpringApplicationBuilder builder = new SpringApplicationBuilder(MyBatchApplication.class);
SpringApplication app = builder.build();
app.setRegisterShutdownHook(false);
ConfigurableApplicationContext context = app.run(args);
contextRef.set(context);

JobExecution jobExecution = LoggingJobExecutionListener.getLastJobExecution();
exitCode.set(SpringApplication.exit(context));

if (!jobExecution.getExitStatus().getExitCode().equals("COMPLETED")) {
throw new RuntimeException("Batch Job Failed: " + jobExecution.getExitStatus().getExitDescription());
}

log.info("Application exiting with code: " + exitCode.get());

} catch (Exception e) {
log.error("Exception caught in main: {}", e.getMessage(), e);
exitCode.set(1);

ConfigurableApplicationContext ctx = contextRef.get();
if (ctx != null && ctx.isActive()) {
exitCode.set(SpringApplication.exit(ctx));
}

log.error("Application will exit with code: {}", exitCode.get());
throw e; // CallRunner까지 예외 전파
}

System.exit(exitCode.get());
}
  • System.exit(exitCode.get())을 명확히 실행하여 Kubernetes Pod 상태가 정확히 반영됨
  • throw e;로 CallRunner까지 예외가 정상적으로 전달됨
  • setRegisterShutdownHook(false); 설정을 추가하여 Spring의 기본 종료 훅이 중복 실행되지 않도록 함

관련 글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다