tomcat패키지의 GracefulShutdown 클래스 일부
private void doShutdown(GracefulShutdownCallback callback) {
List<Connector> connectors = this.getConnectors();
connectors.forEach(this::close);
try {
Container[] var3 = this.tomcat.getEngine().findChildren();
int var4 = var3.length;
for (int var5 = 0; var5 < var4; ++var5) {
Container host = var3[var5];
Container[] var7 = host.findChildren();
int var8 = var7.length;
for (int var9 = 0; var9 < var8; ++var9) {
Container context = var7[var9];
while (!this.aborted && this.isActive(context)) { // 해당 컨테이너가 비동기 작업을 실행중이거나, 사용중인 자원이 있다면 isActive는 true
Thread.sleep(50L);
}
if (this.aborted) { // 프로퍼티 파일에서 설정한 타임아웃이 지났을 경우 abort된다.
logger.info("Graceful shutdown aborted with one or more requests still active");
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
return;
}
}
}
} catch (InterruptedException var11) {
Thread.currentThread().interrupt();
}
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
}
타임아웃 시간까지 스레드가 종료되지 않으면 인터럽트된다.
타임아웃 시간안에 종료되지 못한 비동기 스레드(비동기 작업을 실행하는 스레드)들을 안전하게 재처리하기 위해 추가적인 설정이 있으면 좋을 것 같았다.
동기로 동작하는 API요청의 경우 사용자가 에러 응답을 받기 때문에 재시도 요청을 빠르게 할 수 있지만, Event로 발행되는 비동기 스레드에서 인터럽트가 발생할 경우 재시도를 하기가 번거로울 것 같았다. 그래서 인터럽트 될 비동기 스레드들을 자세하게 로깅하는 기능을 추가해보려고 한다. (비동기 스레드가 시작될 때 체크하고, 종료될 때 체크해서 시작만 체크된 비동기 스레드를 확인하는 방법도 있겠지만 셧다운으로 인해 종료될 확률은 극히 드문 케이스일 것 같으니 매번 체크하기보단 인터럽트가 되었을 때만 체크하는 방식이 좋아보였음)
예외를 처리하기위한 기능으로 ControllerAdvice가 있지만, ControllerAdvice의 경우는 Controller = api로 들어온 요청만 캐치하기 때문에 비동기 작업을 수행하는 스레드나, 스케줄링 등으로 실행되는 스레드를 캐치하긴 어렵다.
챗지피티한테 물어봤을 때, Application 클래스에 @PreDestory, @PostDestroy메서드를 만들어서 셧다운 이후에도 실행중인 스레드들에 대해 로깅을 처리하는 것을 추천해줬지만, 테스트 결과 @PreDestory보다 doShutdown()이 먼저 호출되어서 스레드들이 전부 인터럽트된 이후에 @PreDestory가 동작했다.
Applilcation이 셧다운 되는 명령을 받았고, GracefulShutdown의 doShutdown메서드가 호출되기 전에 인터셉트할 수 있는 기능을 찾아봤는데 챗지피티가 ApplicationListener<ContextClosedEvent>를 추천해줬다.
다음과 같이 application의 close()가 호출된 이후에 실행할 메서드를 구현할 수 있다.
@Component
class ShutdownCustom(
private val checkAtShutdownThreadContext: CheckAtShutdownThreadContext
) : ApplicationListener<ContextClosedEvent> {
override fun onApplicationEvent(event: ContextClosedEvent) {
val threadContext: Map<String, MethodArgs> = checkAtShutdownThreadContext.getContext()
// Map 순회
for ((threadName, methodArgs) in threadContext) {
// 로깅 예시
println("Thread: $threadName")
println(" Method: ${methodArgs.method.declaringClass.simpleName}.${methodArgs.method.name}()")
println(" Params: ${methodArgs.args.joinToString()}")
}
}
}
나는 셧다운에 의해 강제로 종료될 비동기 스레드들의 메서드명과 파라미터를 알고 싶어 다음과 같이 AOP와 커스텀 어노테이션을 생성했다.
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class CheckAtShutdown
@Aspect
@Component
class CheckAtShutdownThreadContext {
// 스레드 이름 → Method 및 파라미터 저장용 DTO
private val context: MutableMap<String, MethodArgs> = ConcurrentHashMap()
// DTO 클래스
data class MethodArgs(val method: Method, val args: Array<Any?>)
// @CheckAtShutdown 가 붙은 메서드만 포인트컷
@Pointcut("@annotation(com.example.trace.CheckAtShutdown)")
fun checkAtShutdownMethods() {
// pointcut signature method
}
@Around("checkAtShutdownMethods()")
@Throws(Throwable::class)
fun aroundCheck(pjp: ProceedingJoinPoint): Any? {
val threadName = Thread.currentThread().name
val sig = pjp.signature as MethodSignature
val method = sig.method
val args = pjp.args
// 진입 시점에 메서드 파라미터들 저장
context[threadName] = MethodArgs(method, args)
return try {
pjp.proceed()
} finally {
// 완료 시점에 컨텍스트에서 제거
context.remove(threadName)
}
}
// ContextClosedEvent에서 호출해 사용
fun getContext(): Map<String, MethodArgs> =
Collections.unmodifiableMap(context)
}