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

2025. 6. 4. 22:39·Back-end/Spring
반응형

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() 해주었다.

반응형

'Back-end > Spring' 카테고리의 다른 글

[Spring] React에서 Set-Cookie가 보이지 않아요  (0) 2025.04.23
[Spring] filter vs AOP vs Interceptor  (0) 2025.04.09
[Spring] Docker-Compose를 활용한 CI / CD 구축  (1) 2025.03.18
[Spring] com.auth0 vs jsonwebtoken.jjwt  (1) 2025.02.16
[Spring] JPA 벌크 연산이란?  (0) 2025.02.01
'Back-end/Spring' 카테고리의 다른 글
  • [Spring] React에서 Set-Cookie가 보이지 않아요
  • [Spring] filter vs AOP vs Interceptor
  • [Spring] Docker-Compose를 활용한 CI / CD 구축
  • [Spring] com.auth0 vs jsonwebtoken.jjwt
류건
류건
개발 일지
  • 류건
    건's Dev
    류건
  • 전체
    오늘
    어제
    • 분류 전체보기 (96)
      • Back-end (56)
        • Spring (31)
        • Nest.js (3)
        • Next.js (2)
        • Node.js (3)
      • Infra & Cloud (21)
        • Cloud Computing (6)
        • Docker (3)
        • AWS (7)
      • Java (2)
      • Computer Science (12)
        • Computer Network (0)
        • Operating System (0)
        • 정보 보호와 시스템 보안 (12)
        • Software Architecture (0)
      • 회고록 (4)
        • 우아한테크코스 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    티스토리챌린지
    EC2
    오블완
    Kafka
    https
    Github Actions
    Nest.js
    nginx
    aws
    Lock
    고가용성
    Spring
    Webflux
    JWT
    ssl
    CD
    Docker
    정보보호
    Spring Boot
    db
    보안
    JPA
    node.js
    CORS
    어노테이션
    WebClient
    CI/CD
    express.js
    ddl-auto
    public key
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.0
류건
[Spring] WebClient 라이브러리를 활용한 외부 API 호출 시 발생하는 scanAvailable 에러
상단으로

티스토리툴바