[Spring] WebClient 라이브러리를 활용한 외부 API 호출 시 발생하는 scanAvailable 에러
1. WebClient 라이브러리를 활용하여 GPT 서버 API 호출
사용자가 prompt를 입력하면, 이 prompt를 Request Body로 담아주고 GPT 서버 API를 호출하면 API는 응답 값으로 String 배열을 리턴해준다. 이 API의 response body를 받아와서 배열 내 문자열을 바탕으로 각 Bubble이라는 엔티티 객체를 만들어 DB에 저장하는 로직을 구현하고자 하였다.
@PostMapping("/create")
@Operation(summary = "버블 생성 API", description = "프롬프트를 기준으로 GPT API에서 뽑아준 청크 단위로 버블 생성 후 반환")
public ApiResponse<List<BubbleDTO>> createBubbles(
@AuthenticationPrincipal CustomOAuth2User user,
@Valid @RequestBody PromptRequest request
) {
Member member = memberService.findById(user.getMemberId());
return ApiResponse.ok(webFluxService.createBubbles(request, member));
}
package capstone.backend.domain.bubble.service;
import capstone.backend.domain.bubble.dto.request.PromptRequest;
import capstone.backend.domain.bubble.dto.response.BubbleDTO;
import capstone.backend.domain.bubble.dto.response.PromptResponse;
import capstone.backend.domain.bubble.entity.Bubble;
import capstone.backend.domain.bubble.repository.BubbleRepository;
import capstone.backend.domain.member.scheme.Member;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class WebFluxService {
private final WebClient webClient;
private final BubbleRepository bubbleRepository;
@Value("${url.path.model.prompt}")
private String gptServerEndpoint;
// memberRepository에서 직접 조회하는 경우 동기적으로 작동함
// 따라서, 한 트랜잭션 안에 member를 조회하는 로직을 제외하고 Member 객체를 받게 로직 설정
@Transactional
public Mono<List<BubbleDTO>> createBubbles(PromptRequest request, Member member) {
log.info("GPT API 호출");
return webClient.post()
.uri(gptServerEndpoint)
.bodyValue(request)
.retrieve()
.bodyToMono(PromptResponse.class)
.map(response -> {
return response.chunks().stream()
.map(chunk -> {
Bubble bubble = Bubble.create(chunk, member);
bubbleRepository.save(bubble);
log.info("Bubbles 저장 완료: bubble = {}", chunk);
return new BubbleDTO(bubble);
})
.toList();
});
}
}
🔥 1차 문제 발생
여기서 Mono 객체를 받고자 하였지만, 모든 response가 다음과 같은 형식으로 리턴되었다.
{
"statusCode": 200,
"error": null,
"content": {
"scanAvailable": true
}
}
뭐가 문제지..하고 고민하는 과정에서 저 scanAvailable은 Webflux 객체 리턴 값이 Mono / Flux가 아닌 정상적으로 들어오지 않은 경우 리턴되는 값이다.
즉, content 값이 엉뚱하게 들어가는 것인데, 알고 보니 ApiResponse.ok()가 호출될 때, Mono 자체가 content에 들어가지 않게끔 wrapping이 제대로 안 됐거나, 혹은 ApiResponse 클래스에서 Mono를 unwrap 안 하고 그대로 serialize 하게 된 것이다.
따라서 Controller 코드를 다음과 같이 수정했다.
@PostMapping("/create")
@Operation(summary = "버블 생성 API", description = "프롬프트를 기준으로 GPT API에서 뽑아준 청크 단위로 버블 생성 후 반환")
public Mono<ApiResponse<List<BubbleDTO>>> createBubbles(
@AuthenticationPrincipal CustomOAuth2User user,
@Valid @RequestBody PromptRequest request
) {
Member member = memberService.findById(user.getMemberId());
return webFluxService.createBubbles(request, member)
.map(ApiResponse::ok);
}
🔥 2차 문제 발생
분명히 Spring API를 호출할 때 JWT를 헤더에 추가하고 API를 요청했지만, Spring Security 필터를 거치지 못해서 oauth2 로그인이 필요하다는 html이 응답되었다.
과정
- WebClient로 FastAPI API 호출
- 호출 중에 Spring Security Filter가 작동하면서 WebClient 호출 자체가 “인증이 필요한 protected 자원 호출로 감지” → Spring Security가 OAuth2 로그인 페이지로 넘기려 함
- WebClient 호출할 때도 기본적으로 SecurityContext가 적용돼 있는데, 거기 Authentication이 없으니 Spring이 막으려는 것
해결 방안
따라서, Security Context 완전 무시하는 WebClient 강제 설정을 적용한다.
WebClient Builder에서 defaultRequest나 Security context filter 완전 제거한 Bean으로 등록하기.
따라서, WebClient에 대해 Filter를 거치지 않도록 SecurityConfig.java에 requestMatcher에 등록하고 permitAll() 해주었다.