기존의 챗봇 서비스에서 처음에 구독을 요청해서 SseEmitter를 응답받는 API, Gemini에 질문을 요청해서 처음에 응답받은 Emitter로 답변을 보내는 API 두개로 나눠져있었다.
이것을 하나의 API로 합치려고, Emitter는 먼저 응답받고 Gemini질문은 비동기로 처리하도록 변경했다.



Suppressed: java.lang.NullPointerException: Cannot invoke "org.springframework.web.context.request.ServletRequestAttributes.getRequest()" because "attributes" is null
at com._42195km.msa.common.config.AuditorAwareImpl.getCurrentAuditor(AuditorAwareImpl.java:22) ~[main/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:569) ~[na:na]
AuditorAwareImpl의 attribute에서 NullPointerExceiption이 발생했다.
chatbot 로직에서 답변이 완료되면 대화 내역을 저장하게 되는데, createBy(작성자)를 AuditorAware에서 처리해준다.
아래 코드의 (ServletRequestAttributes)RequestContextHolder.getRequestAttributes()부분에서 NullPointerExceiption이 발생했다.
@Component
@Slf4j
public class AuditorAwareImpl implements AuditorAware<UUID> {
@Override
public Optional<UUID> getCurrentAuditor() {
// 이 부분에서 NullPointerExceiption이 발생
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
log.info(">>> X-User-Id: {}", attributes.getRequest().getHeader("X-User-Id"));
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String userId = request.getHeader("X-User-Id");
if (userId != null) {
try {
return Optional.of(UUID.fromString(userId));
} catch (IllegalArgumentException e) {
return Optional.empty();
}
}
}
return Optional.empty();
}
}
chatbot 답변 로직을 비동기로 처리로 바꾸면서 새로운 쓰레드에서 실행이 됐다.
RequestContextHolder는 Spring에서 전역으로 Request에 대한 정보를 가져오고자 할 때 사용하는 유틸성 클래스이다.
RequestContextHolder는 내부적으로 ThreadLocal을 사용하여 각 쓰레드에게 현재 요청 객체를 제공한다.
그래서 비동기로 실행된 새로운 쓰레드에서 기존 쓰레드의 ThreadLocal에 저장된 RequestContextHolder를 얻어 올 수 없기 때문에 attribute에 값이 없어 NullPointerException이 발생했다.
public abstract class RequestContextHolder {
private static final boolean jsfPresent = ClassUtils.isPresent("jakarta.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
public RequestContextHolder() {
}
public static void resetRequestAttributes() {
requestAttributesHolder.remove();
inheritableRequestAttributesHolder.remove();
}
public static void setRequestAttributes(@Nullable RequestAttributes attributes) {
setRequestAttributes(attributes, false);
}
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
} else if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
} else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
if (attributes == null) {
attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
}
return attributes;
}
.....
}
RequestContextHolder 내부를 살펴보면 ThreadLocal에 RequestAttributes를 저장하는 것을 볼 수 있다.
TaskDecorator를 이용해 기존 쓰레드의 RequestContextHolder를 새로운 쓰레드에 설정해준다.
Task Decorator는 thread-pool로 작업(Task)을 시작하기 직전에 진행할 작업을 추가할 수 있도록 만든 interface이다.
RequestContextHolder를 새로운 쓰레드의 ThreadLocal에 set()하여 새로운 쓰레드에서도 기존 쓰레드의 컨텍스트 정보를 알 수 있게된다.
@Configuration
public class AsyncConfig {
@Bean
public ThreadPoolTaskExecutor asyncThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new CopyTaskDecorator());
return executor;
}
private static class CopyTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return () -> {
try {
// 새 스레드에 이전 컨텍스트를 설정
RequestContextHolder.setRequestAttributes(attributes);
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
}
}
}
