diff --git "a/blog/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254.mdx" "b/blog/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254.mdx" new file mode 100644 index 00000000..e5009351 --- /dev/null +++ "b/blog/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254.mdx" @@ -0,0 +1,111 @@ +--- +title: 비동기 예외 처리 +slug: async-exception +authors: [herb] +tags: [async, exception] +--- + +### 개요 + +현재 트립드로우의 경로 이미지 생성 기능은 비동기로 처리되고 있습니다. +로그를 확인하는 도중 `@Async`가 적용된 메서드에서 예외가 발생하는 경우 로그가 정상적으로 출력되지 않는 문제를 확인했습니다. + +확인해보니 Spring의 `@ControllerAdvice` + `@ExceptionHandler`의 경우 동기 예외만 처리하고, 비동기 예외를 처리하지 않았습니다. +따라서 Spring에서 지원하는 `AsyncUncaughtExceptionHandler` 인터페이스를 구현해 예외를 처리하는 클래스를 생성했습니다. + +### 비동기 예외처리 + +```java title=AsyncExceptionHandler +@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`를 반환하도록 설정했습니다. +이렇게 설정한다면 예외가 발생하는 경우 `AsyncUncaughtExceptionHandler`의 구현체인 `AsyncExceptionHandler`가 예외를 잡아 처리합니다. + +```java title=AsyncConfig +@EnableAsync +@Configuration +public class AsyncConfig implements AsyncConfigurer { + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new AsyncExceptionHandler(); + } +} +``` + +### MDC 정보 연동 문제 + +![./mdc-null.png](./mdc-null.png) + +현재 예외가 발생할 때 실행 흐름을 추적하기 위해 MDC(Mapped Diagnostic Context) 를 사용하고 있습니다. +비동기 처리의 경우 별도의 스레드에서 동작하기 때문에 ThreadLocal 기반으로 동작하는 MDC의 정보를 얻어올 수 없었습니다. + +이를 적절하게 Decorator 클래스를 설정하여 MDC의 정보를 복사해서 넘겨줄 수 있습니다. + +다음과 같이 TaskDecorator를 구현한 클래스를 하나 생성하고, Task가 실행되기 전 MDC의 정보를 복사하도록 설정했습니다. + +```java title=MdcTaskDecorator +public class MdcTaskDecorator implements TaskDecorator { + + @Override + public Runnable decorate(final Runnable runnable) { + Map threadContext = MDC.getCopyOfContextMap(); + return () -> { + MDC.setContextMap(threadContext); + runnable.run(); + }; + } +} +``` + +해당 Decorator 클래스를 설정 파일에 등록해줍니다. + +```java title=AsyncConfig +@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()); + + // highlight-next-line + executor.setTaskDecorator(new MdcTaskDecorator()); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.initialize(); + return executor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new AsyncExceptionHandler(); + } +} +``` + +설정 후에는 정상적으로 MDC에 들어가있는 UUID가 출력되는 것을 볼 수 있습니다. + +![./mdc-not-null.png](./mdc-not-null.png) + +### 참고 자료 + +[spring async, baeldung](https://www.baeldung.com/spring-async) +[@Async will not call by @ControllerAdvice for global exception](https://stackoverflow.com/questions/61885358/async-will-not-call-by-controlleradvice-for-global-exception) +[Spring 의 동기, 비동기, 배치 처리시 항상 context 를 유지하고 로깅하기, 강남언니](https://blog.gangnamunni.com/post/mdc-context-task-decorator/) \ No newline at end of file diff --git "a/blog/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254/mdc-not-null.png" "b/blog/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254/mdc-not-null.png" new file mode 100644 index 00000000..65884710 Binary files /dev/null and "b/blog/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254/mdc-not-null.png" differ diff --git "a/blog/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254/mdc-null.png" "b/blog/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254/mdc-null.png" new file mode 100644 index 00000000..661c9afe Binary files /dev/null and "b/blog/2023-09-18-\353\271\204\353\217\231\352\270\260 \354\230\210\354\231\270 \354\262\230\353\246\254/mdc-null.png" differ