Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 비동기 예외 처리 기능 추가 (#368) #372

Merged
merged 3 commits into from
Sep 26, 2023

Conversation

greeng00se
Copy link
Member

@greeng00se greeng00se commented Sep 18, 2023

📌 관련 이슈

📁 작업 설명

현재 경로 이미지 생성 기능은 비동기로 처리되고 있습니다.
로그를 확인하는 도중 @Async가 적용된 메서드에서 예외가 발생하는 경우 로그가 정상적으로 출력되지 않는 문제를 확인했습니다.

스크린샷 2023-09-18 오전 11 40 03

확인해보니 Spring의 @ControllerAdvice + @ExceptionHandler의 경우 동기 예외만 처리해주고, 비동기 예외를 처리해주지 않았습니다.
따라서 Spring에서 지원해주는 AsyncUncaughtExceptionHandler 인터페이스를 구현해서 예외를 처리해주는 클래스를 생성하였습니다.

비동기 예외처리

@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가 예외를 잡아서 처리를 해주었습니다.

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

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

MDC 정보 연동 문제

기존 예외가 발생할 때 실행 흐름을 추적하기 위해 MDC(Mapped Diagnostic Context) 를 사용하고 있었습니다.
비동기 처리의 경우 별도의 스레드에서 동작하기 때문에 ThreadLocal 기반으로 동작하는 MDC의 정보를 얻어올 수 없었습니다.

이를 적절하게 Decorator 클래스를 설정하여 MDC의 정보를 복사해서 넘겨줄 수 있습니다.

다음과 같이 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가 출력되었습니다!

스크린샷 2023-09-18 오전 11 48 15

@github-actions
Copy link

github-actions bot commented Sep 18, 2023

Test Results

238 tests   238 ✔️  17s ⏱️
  67 suites      0 💤
  67 files        0

Results for commit 11c4ef1.

♻️ This comment has been updated with latest results.

Copy link
Collaborator

@Jaeyoung22 Jaeyoung22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다 질문 커멘트로 남겨놨습니다

executor.setMaxPoolSize(properties.maxSize());
executor.setQueueCapacity(properties.queueCapacity());
executor.setTaskDecorator(new MdcTaskDecorator());
executor.setWaitForTasksToCompleteOnShutdown(true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이게 의미하는 건 뭔가요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 좋은 질문입니다. 👍
해당 부분은 애플리케이션 종료를 할 때 진행 중인 작업이 있으면 진행 중인 작업을 완료하고, 애플리케이션 종료를 하도록 하는 설정입니다!

pool:
core-size: 8
queue-capacity: 100
max-size: 16
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 수치들은 어떻게 나온 수치들인가요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스프링부트의 기본 설정값입니다 👍

Copy link
Collaborator

@Combi153 Combi153 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다~~
어려운 거 잘 배웠어요ㅎㅎ

Copy link
Collaborator

@Jaeyoung22 Jaeyoung22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

답변 감사합니다
컨플릭트 해결하고 머지 부탁드립니다

@greeng00se greeng00se merged commit bc31e3b into develop-backend Sep 26, 2023
3 checks passed
@greeng00se greeng00se deleted the feature/#368 branch September 26, 2023 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants