Back-end/Spring

[Spring] WebClient 라이브러리를 활용한 외부 API 호출 시 발생하는 scanAvailable 에러

류건 2025. 6. 4. 22:39
반응형

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이 응답되었다.

 

 

과정

  1. WebClient로 FastAPI API 호출
  2. 호출 중에 Spring Security Filter가 작동하면서 WebClient 호출 자체가 “인증이 필요한 protected 자원 호출로 감지” → Spring Security가 OAuth2 로그인 페이지로 넘기려 함
  3. WebClient 호출할 때도 기본적으로 SecurityContext가 적용돼 있는데, 거기 Authentication이 없으니 Spring이 막으려는 것

 

해결 방안

따라서, Security Context 완전 무시하는 WebClient 강제 설정을 적용한다.

WebClient Builder에서 defaultRequest나 Security context filter 완전 제거한 Bean으로 등록하기.

따라서, WebClient에 대해 Filter를 거치지 않도록 SecurityConfig.java에 requestMatcher에 등록하고 permitAll() 해주었다.

반응형