본문으로 건너뛰기

· 약 4분

문제 상황​

현재 트립드로우의 경로 이미지 생성 기능은 비동기로 처리되고 있다. 로그를 확인하는 도중 @Async가 적용된 메서드에서 예외가 발생하는 경우 로그가 정상적으로 출력되지 않는 문제가 발생했다.
확인해 보니 Spring의 @ControllerAdvice + @ExceptionHandler의 경우 동기 예외만 처리하고, 비동기 예외를 처리하지 않았기 때문에 발생한 문제였다.

비동기 예외 발생시 로깅 설정​

스프링 4.1 부터 제공되는 AsyncUncaughtExceptionHandler의 경우 반환 타입이 void인 비동기 메서드를 예외 처리하기 쉽도록 도와준다.

따라서 AsyncUncaughtExceptionHandler 인터페이스를 구현해서 예외를 핸들링하는 클래스를 생성했다.
기존의 동기 예외 처리의 경우 예외가 발생할 때 실행 흐름을 추적하기 위해 MDC(Mapped Diagnostic Context)를 사용하고 있다.
비동기 예외 처리도 마찬가지로 MDC의 정보를 가져와서 로그를 출력하도록 설정했다.

@Slf4j
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

private static final String LOG_FORMAT = "[%s] %s";

@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
log.info(String.format(LOG_FORMAT, MDC.get(REQUEST_ID.key()), throwable.getMessage()), throwable);
}
}

AsyncExceptionHandler의 경우 AsyncConfigurer를 구현한 Configuration 클래스를 사용하여 등록할 수 있다.
getAsyncUncaughtExceptionHandler() 메서드를 오버라이딩하여 AsyncExceptionHandler를 반환하도록 설정하면 된다.

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
}

이제 비동기 상황에서 예외가 발생하는 경우 AsyncUncaughtExceptionHandler의 구현체인 AsyncExceptionHandler가 예외를 잡아 처리한다.

MDC 정보 연동 문제​

비동기 처리의 경우 별도의 스레드에서 동작하기 때문에 ThreadLocal 기반으로 동작하는 MDC의 정보를 얻어올 수 없는 문제가 발생했다.

./mdc-null.png

스프링 4.3 이상부터 제공되는 TaskDecorator를 이용하면 TaskExecutor를 커스터마이징 할 수 있다. TaskDecorator를 구현한 클래스를 하나 생성하고, Task가 실행되기 전 MDC의 정보를 복사하도록 설정한다.

public class MdcTaskDecorator implements TaskDecorator {

@Override
public Runnable decorate(final Runnable runnable) {
Map<String, String> threadContext = MDC.getCopyOfContextMap();
return () -> {
MDC.setContextMap(threadContext);
runnable.run();
};
}
}

생성한 Decorator 클래스를 설정 파일에 등록해 준다.

@RequiredArgsConstructor
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

private final AsyncConfigurationProperties properties;

@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(properties.coreSize());
executor.setMaxPoolSize(properties.maxSize());
executor.setQueueCapacity(properties.queueCapacity());

executor.setTaskDecorator(new MdcTaskDecorator());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
}

설정 후에는 정상적으로 MDC에 들어가 있는 UUID가 출력되는 것을 볼 수 있다.

./mdc-not-null.png

참고 자료​

spring async, baeldung
@Async will not call by @ControllerAdvice for global exception
Spring 의 동기, 비동기, 배치 처리시 항상 context 를 유지하고 로깅하기, 강남언니
TaskDecorator, Spring docs
AsyncUncaughtExceptionHandler