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. 해결 방법
예외가 CallRunner까지 정상적으로 전파되도록 설정
throw e;
을 명확히 사용하여 CallRunner에서 예외를 감지할 수 있도록 함.
System.exit()
실행 위치 조정
- Shutdown Hook 내에서
System.exit()
을 호출하지 않음. - CallRunner 실행 후 JVM 종료 코드만 제대로 반영되도록 처리.
Kubernetes Pod 종료 상태 처리 (exitCode
설정)
exitCode = 0
→ 정상 종료 (Completed
)exitCode ≠ 0
→ 비정상 종료 (Error
),CrashLoopBackOff
가능성 있음restartPolicy: Never
또는restartPolicy: OnFailure
로 조정
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의 기본 종료 훅이 중복 실행되지 않도록 함