Spring Boot 애플리케이션을 Kubernetes 환경에서 실행할 때, 비정상 종료(System.exit(1)
) 처리를 적절히 하지 않으면 두 가지 주요 문제가 발생할 수 있다.
- ApplicationContext가 닫힌 후
SpringApplication.exit()
을 호출하면 발생하는IllegalStateException
오류 - Pod가 예상치 못하게 종료되면서 Kubernetes에서
CrashLoopBackOff
상태로 빠지는 문제
이 문제는 Spring의 종료 프로세스를 올바르게 관리하지 않기 때문에 발생한다. 이를 해결하려면 비정상 종료 시에도 Spring의 컨텍스트 종료 이벤트(ContextClosedEventListener
)를 정상적으로 실행하도록 설정해야 한다.
문제 원인 분석
IllegalStateException: ApplicationContext has been closed already
오류 발생
비정상 종료 시 SpringApplication.exit(configurableApplicationContext);
을 실행하면, 이미 닫힌 ApplicationContext를 참조할 가능성이 있다.
이 경우, 아래와 같은 예외가 발생할 수 있다.
java.lang.IllegalStateException: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@48ea2003 has been closed already
at org.springframework.context.support.AbstractApplicationContext.assertBeanFactoryActive(AbstractApplicationContext.java:1150)
at org.springframework.context.support.AbstractApplicationContext.getBeansOfType(AbstractApplicationContext.java:1311)
at org.springframework.boot.SpringApplication.exit(SpringApplication.java:1343)
이는 System.exit(1);
이 너무 빨리 실행되거나, Spring이 종료되면서 ContextClosedEventListener
가 완전히 실행되지 않았을 때 발생할 수 있다.
Kubernetes에서 CrashLoopBackOff
발생
System.exit(1);
을 호출하면 Kubernetes는 해당 Pod가 비정상 종료되었다고 판단하고, 자동으로 새로운 Pod를 생성한다.
하지만 애플리케이션이 적절한 종료 절차 없이 즉시 종료되면, Kubernetes가 이를 반복적으로 감지하여 CrashLoopBackOff
상태가 발생할 수 있다.
이 문제를 해결하려면:
SpringApplication.exit()
을 먼저 실행하여 Spring의 종료 프로세스를 정상적으로 수행- 그 후
System.exit(1);
을 실행하여 Kubernetes가 비정상 종료로 감지하고 Pod를 재시작할 수 있도록 해야 한다.
해결 방법
configurableApplicationContext.isActive()
체크 후 exit()
호출
SpringApplication.exit()
을 호출하기 전에 ApplicationContext가 닫히지 않았는지 확인하는 것이 중요하다.
이미 닫힌 컨텍스트에 대해 exit()
을 호출하면 IllegalStateException
이 발생하므로, 이를 방지하려면 isActive()
또는 isRunning()
체크를 수행해야 한다.
수정된 코드 (job.name
→ batch.process.name
변경 포함)
ConfigurableApplicationContext configurableApplicationContext = null; // try 바깥에서 선언
try {
if (log.isDebugEnabled()) {
log.debug("###############batch.process.name = " + System.getProperty("batch.process.name"));
}
SpringApplicationBuilder springApplicationBuilder = new SpringApplicationBuilder(Application.class);
SpringApplication springApplication = springApplicationBuilder.build();
// try 바깥에서 선언한 변수에 값 할당
configurableApplicationContext = springApplication.run(args);
JobExecution jobExecution = LoggingJobExecutionListener.getLastJobExecution();
if (log.isDebugEnabled()) {
log.debug("jobExecution.getInstanceId()=" + jobExecution.getJobInstance().getInstanceId());
log.debug("jobExecution.getJobName()=" + jobExecution.getJobInstance().getJobName());
log.debug("jobExecution.getExitCode()=" + jobExecution.getExitStatus().getExitCode());
log.debug("jobExecution.getId()=" + jobExecution.getId());
}
// Batch Process가 실패한 경우 강제로 예외 발생
if (!jobExecution.getExitStatus().getExitCode().equals("COMPLETED")) {
log.error("Batch Process Failed: " + jobExecution.getExitStatus().getExitDescription());
throw new BizException("Batch Process Failed: " + jobExecution.getExitStatus().getExitDescription());
}
// 정상적으로 종료되었을 경우
int exitCode = SpringApplication.exit(configurableApplicationContext);
System.exit(exitCode);
} catch (Exception e) {
log.error("예외 발생! 정리 후 종료 시도", e);
int exitCode = 1; // 기본적으로 비정상 종료 코드 설정
if (configurableApplicationContext != null && configurableApplicationContext.isActive()) {
exitCode = SpringApplication.exit(configurableApplicationContext);
}
log.info("Application will exit with code: {}", exitCode);
// **System.exit()을 호출하여 Kubernetes가 Pod를 재시작하도록 유도**
System.exit(exitCode);
}
Kubernetes에서 정상적으로 종료하도록 설정
Kubernetes에서 비정상 종료가 발생해도 CrashLoopBackOff를 방지하려면, preStop
훅을 추가하여 컨테이너 종료 전에 정리할 시간을 주는 것도 고려할 수 있다.
preStop
훅 설정 (선택 사항)
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
이렇게 설정하면, Pod가 종료될 때 10초 동안 종료 작업을 수행할 시간을 확보할 수 있다.
결론
- 비정상 종료 시에도 Spring의 종료 프로세스를 정상적으로 수행해야 한다.
SpringApplication.exit(configurableApplicationContext)
을 호출하여 Spring이ContextClosedEvent
를 정상적으로 발생시키도록 한다.
- 이미 닫힌 컨텍스트에서
SpringApplication.exit()
을 호출하면IllegalStateException
이 발생할 수 있다.- 이를 방지하려면
configurableApplicationContext.isActive()
체크 후 실행해야 한다.
- 이를 방지하려면
- Kubernetes에서 Pod가 재시작될 수 있도록
exit code 1
을 유지해야 한다.System.exit(1);
을 호출하면 Kubernetes가 이를 감지하고 새 Pod를 생성한다.- 하지만
System.exit(1);
을 호출하기 전에 Spring의 종료 이벤트(ContextClosedEventListener
)가 실행될 시간을 확보해야 한다.
- 추가적인 안정성을 위해 Kubernetes의
preStop
훅을 활용할 수도 있다.