-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
661c2a0
commit 1282cf8
Showing
3 changed files
with
111 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, String> 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/) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.