본문으로 건너뛰기

"webclient" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

모든 태그 보기

Spring Boot에서 FastAPI와 통신하는 방법

· 약 9분
dev-burnern
Developer

Spring Boot로 백엔드 서버를 만들다 보면 모든 기능을 하나의 서버에서 처리할 수도 있다. 하지만 AI 기능이 들어가면 이야기가 조금 달라진다.

예를 들어 회원 관리, 로그인, 결제, 게시글, 템플릿 관리는 Spring Boot가 담당하고, RAG 검색, LLM 응답 생성, 코드 분석, 문서 요약 같은 AI 기능은 FastAPI가 담당하도록 나눌 수 있다.

Frontend

Spring Boot

FastAPI

이 구조에서 중요한 질문은 하나다.

Spring Boot와 FastAPI는 어떤 방식으로 통신해야 할까?


1. Spring Boot와 FastAPI를 함께 사용하는 이유

Spring Boot는 Java 기반 백엔드 서버를 만들 때 많이 사용된다.

회원, 인증, 권한, 결제, 게시글, 관리자 기능처럼 서비스의 핵심 기능을 안정적으로 처리하기 좋다.

반면 FastAPI는 Python 기반 웹 프레임워크이다.

Python은 AI, 머신러닝, 데이터 처리, RAG, LLM 관련 라이브러리와 잘 어울린다. 그래서 AI 기능을 구현할 때는 FastAPI를 따로 두는 것이 편하다.

예를 들어 다음과 같이 역할을 나눌 수 있다.

서버주요 역할
Spring Boot회원, 인증, 결제, 템플릿 관리, API Gateway 역할
FastAPIAI 응답 생성, RAG 검색, 코드 분석, 문서 요약

정리하면 Spring Boot와 FastAPI를 함께 사용하는 이유는 다음과 같다.

Spring Boot는 서비스의 핵심 로직을 담당하고, FastAPI는 AI 기능을 담당하도록 책임을 분리하기 위해서이다.


2. 가장 기본적인 방식은 REST API 통신이다

Spring Boot와 FastAPI가 통신하는 가장 기본적인 방법은 REST API 방식이다.

쉽게 말하면 Spring Boot가 FastAPI의 API 주소로 HTTP 요청을 보내고, FastAPI는 그 요청을 처리한 뒤 JSON 형태로 응답을 돌려준다.

예를 들어 사용자가 AI 질문을 보냈다고 하자.

사용자 질문 입력

Spring Boot 서버

FastAPI 서버 호출

FastAPI가 AI 응답 생성

Spring Boot가 결과를 받아 사용자에게 반환

이때 Spring Boot는 FastAPI의 API 주소로 요청을 보낸다.

POST http://fastapi-server/api/v1/ai/ask

요청 데이터는 보통 JSON 형태로 보낸다.

{
"question": "JWT와 세션의 차이가 뭐야?"
}

FastAPI는 이 요청을 처리하고 다시 JSON으로 응답한다.

{
"answer": "JWT는 토큰 기반 인증 방식이고, 세션은 서버에 인증 상태를 저장하는 방식입니다.",
"references": ["JWT 문서", "Spring Security 문서"]
}

정리하면 REST API 통신은 다음과 같다.

Spring Boot가 FastAPI의 API를 HTTP로 호출하고, JSON 데이터를 주고받는 방식이다.


3. Spring Boot에서 FastAPI를 호출하는 선택지

Spring Boot에서 FastAPI를 호출하려면 HTTP Client가 필요하다.

대표적인 선택지는 다음과 같다.

선택지특징추천 상황
RestClient동기식 HTTP Client일반적인 요청/응답
WebClient비동기, 논블로킹 HTTP ClientAI 스트리밍, 오래 걸리는 요청
HTTP Interface인터페이스 기반 HTTP Client호출 API가 많을 때
OpenFeign선언형 REST Client기존 프로젝트에서 Feign 사용 중일 때
Message Queue비동기 작업 처리AI 작업이 오래 걸릴 때

4. RestClient

가장 먼저 추천할 수 있는 방식은 RestClient이다.

RestClient는 Spring Boot에서 외부 API를 호출할 때 사용할 수 있는 동기식 HTTP Client이다.

쉽게 말하면 다음과 같은 구조에 잘 맞는다.

Spring Boot가 FastAPI에 요청
→ FastAPI가 응답
→ Spring Boot가 결과를 받아 처리

예를 들어 AI 질문에 대한 답변을 한 번에 받아오는 기능이라면 RestClient로 충분하다.

Spring Boot 예시

package com.example.ai;

import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class FastApiClient {

private final RestClient restClient;

public FastApiClient(RestClient.Builder builder) {
this.restClient = builder
.baseUrl("http://localhost:8000")
.build();
}

public AiAnswerResponse ask(AiAnswerRequest request) {
return restClient.post()
.uri("/api/v1/ai/ask")
.body(request)
.retrieve()
.body(AiAnswerResponse.class);
}
}

요청 DTO는 다음과 같이 만들 수 있다.

package com.example.ai;

public record AiAnswerRequest(
String question
) {
}

응답 DTO는 다음과 같이 만들 수 있다.

package com.example.ai;

import java.util.List;

public record AiAnswerResponse(
String answer,
List<String> references
) {
}

FastAPI 예시

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class AiAnswerRequest(BaseModel):
question: str


class AiAnswerResponse(BaseModel):
answer: str
references: list[str]


@app.post("/api/v1/ai/ask", response_model=AiAnswerResponse)
def ask_ai(request: AiAnswerRequest) -> AiAnswerResponse:
return AiAnswerResponse(
answer=f"질문에 대한 AI 응답: {request.question}",
references=["문서 A", "문서 B"]
)

정리하면 RestClient는 다음과 같다.

Spring Boot에서 FastAPI를 가장 단순하게 호출할 수 있는 동기식 HTTP Client이다.


5. WebClient

WebClient는 비동기, 논블로킹 방식의 HTTP Client이다.

RestClient와 가장 큰 차이는 처리 방식이다.

구분RestClientWebClient
처리 방식동기식비동기, 논블로킹
사용 난이도쉬움조금 어려움
추천 상황일반 API 호출스트리밍, 많은 외부 API 호출
AI 응답 스트리밍부적합적합

예를 들어 ChatGPT처럼 답변이 한 번에 나오는 것이 아니라 조금씩 출력되는 기능을 만들고 싶다면 WebClient가 더 적합하다.

사용자 질문

Spring Boot

FastAPI

AI 응답을 조금씩 스트리밍

Frontend에 실시간 출력

WebClient 예시는 다음과 같다.

package com.example.ai;

import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Component
public class FastApiWebClient {

private final WebClient webClient;

public FastApiWebClient(WebClient.Builder builder) {
this.webClient = builder
.baseUrl("http://localhost:8000")
.build();
}

public Mono<AiAnswerResponse> ask(AiAnswerRequest request) {
return webClient.post()
.uri("/api/v1/ai/ask")
.bodyValue(request)
.retrieve()
.bodyToMono(AiAnswerResponse.class);
}
}

다만 일반적인 요청/응답만 필요하다면 처음부터 WebClient를 사용할 필요는 없다. 구조가 단순한 프로젝트에서는 RestClient가 더 읽기 쉽고 관리하기 편하다.

정리하면 WebClient는 다음과 같다.

AI 응답 스트리밍이나 비동기 처리가 필요할 때 사용하는 Spring의 HTTP Client이다.


6. HTTP Interface

FastAPI에 호출해야 하는 API가 많아지면 HTTP Interface도 고려할 수 있다.

HTTP Interface는 Java 인터페이스에 API 호출 메서드를 정의하고, Spring이 실제 HTTP 호출 객체를 만들어주는 방식이다.

예를 들어 다음과 같이 작성할 수 있다.

package com.example.ai;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

@HttpExchange("/api/v1/ai")
public interface FastApiHttpClient {

@PostExchange("/ask")
AiAnswerResponse ask(@RequestBody AiAnswerRequest request);
}

이 방식은 FastAPI에 호출할 API가 많을 때 유용하다.

예를 들어 AI 서버에 다음과 같은 API가 있다고 하자.

/api/v1/ai/ask
/api/v1/ai/summary
/api/v1/ai/code-review
/api/v1/ai/rag/search

이런 API 호출을 Service 코드 안에 전부 작성하면 코드가 지저분해질 수 있다.

이때 HTTP Interface를 사용하면 API 호출 코드를 인터페이스 중심으로 정리할 수 있다.

정리하면 HTTP Interface는 다음과 같다.

FastAPI 호출 API가 많아졌을 때, 인터페이스 기반으로 HTTP 호출 코드를 깔끔하게 관리하는 방식이다.


7. OpenFeign

OpenFeign도 Spring Boot에서 외부 API를 호출할 때 사용할 수 있는 방식이다.

OpenFeign은 인터페이스를 정의하면 HTTP Client 구현체를 자동으로 만들어주는 선언형 REST Client이다.

예시는 다음과 같다.

@FeignClient(name = "fastApiClient", url = "http://localhost:8000")
public interface FastApiFeignClient {

@PostMapping("/api/v1/ai/ask")
AiAnswerResponse ask(@RequestBody AiAnswerRequest request);
}

OpenFeign은 코드가 깔끔하고 사용하기 편하다.

하지만 새 프로젝트라면 우선순위를 조금 낮게 봐도 된다. Spring Boot 3.x 프로젝트에서는 RestClient, WebClient, HTTP Interface를 먼저 고려하는 것이 좋다.

즉, 기존 프로젝트에서 이미 OpenFeign을 쓰고 있다면 계속 사용해도 된다. 하지만 새로 시작하는 프로젝트라면 다음 순서가 더 적절하다.

RestClient
→ WebClient
→ HTTP Interface
→ OpenFeign

정리하면 OpenFeign은 다음과 같다.

선언형 REST Client이지만, 새 프로젝트에서는 Spring 기본 HTTP Client를 먼저 고려하는 것이 좋다.


8. 오래 걸리는 작업은 메시지 큐를 고려해야 한다

REST API 방식은 단순하고 좋지만, 모든 상황에 적합한 것은 아니다.

예를 들어 다음과 같은 AI 작업은 시간이 오래 걸릴 수 있다.

AI 템플릿 생성
PDF 문서 분석
코드 전체 분석
RAG 문서 임베딩
대량 데이터 요약

이런 작업을 단순 REST API로 처리하면 문제가 생길 수 있다.

Spring Boot가 FastAPI 호출
→ FastAPI 작업이 오래 걸림
→ Spring Boot 요청 대기
→ 타임아웃 발생 가능
→ 사용자 경험 저하

이런 경우에는 메시지 큐를 사용할 수 있다.

Spring Boot
↓ 작업 요청 메시지 발행
RabbitMQ 또는 Kafka

FastAPI Worker
↓ AI 작업 수행
DB 또는 Redis에 결과 저장

Spring Boot가 결과 조회

메시지 큐를 사용하면 Spring Boot는 작업 요청만 보내고 바로 응답할 수 있다. FastAPI Worker는 큐에 쌓인 작업을 하나씩 꺼내서 처리한다.

예를 들어 사용자가 AI 템플릿 생성을 요청했다고 하자.

1. 사용자가 템플릿 생성 요청
2. Spring Boot가 작업 상태를 PENDING으로 저장
3. Spring Boot가 RabbitMQ에 작업 메시지 발행
4. FastAPI Worker가 메시지를 가져와 AI 작업 수행
5. 결과를 DB 또는 Redis에 저장
6. 사용자는 작업 상태 API로 결과 확인

이 구조는 오래 걸리는 작업을 처리할 때 안정적이다.


9. RabbitMQ와 Kafka 비교

메시지 큐를 사용할 때 대표적인 선택지는 RabbitMQ와 Kafka이다.

간단히 비교하면 다음과 같다.

구분RabbitMQKafka
주요 목적작업 큐, 메시지 전달이벤트 스트리밍, 대용량 로그
난이도상대적으로 쉬움상대적으로 어려움
추천 상황AI 작업 요청 처리대량 이벤트 처리
캡스톤 프로젝트 적합도높음과할 수 있음

RabbitMQ는 작업 큐 구조를 만들 때 이해하기 쉽다. 예를 들어 “AI 작업 요청을 큐에 넣고, FastAPI Worker가 하나씩 처리한다”는 식으로 설명하기 좋다.

Kafka는 대량 이벤트 처리나 로그 스트리밍에 강하다. 하지만 설정과 운영 난이도가 RabbitMQ보다 높다.

개인 프로젝트나 캡스톤 프로젝트에서는 Kafka보다 RabbitMQ가 더 현실적인 선택일 수 있다.

정리하면 메시지 큐는 다음과 같다.

오래 걸리는 AI 작업을 바로 처리하지 않고, 작업 요청을 큐에 넣어 비동기로 처리하는 방식이다.


10. 어떤 방식을 선택해야 할까?

Spring Boot와 FastAPI 통신 방식은 작업 성격에 따라 선택하면 된다.

상황추천 방식
일반적인 AI 질문/응답REST API + RestClient
단순 RAG 검색 결과 반환REST API + RestClient
AI 답변을 실시간으로 출력WebClient
FastAPI 호출 API가 많음HTTP Interface
AI 작업이 오래 걸림RabbitMQ
대량 이벤트/로그 처리Kafka
기존 프로젝트에서 Feign 사용 중OpenFeign

개인 프로젝트나 캡스톤 프로젝트라면 처음부터 복잡하게 시작할 필요는 없다.

가장 현실적인 순서는 다음과 같다.

1단계: RestClient로 REST API 통신 구현
2단계: AI 스트리밍이 필요하면 WebClient 도입
3단계: 오래 걸리는 작업이 생기면 RabbitMQ 도입
4단계: API 호출 코드가 많아지면 HTTP Interface로 정리

정리하면 선택 기준은 다음과 같다.

짧은 요청은 REST API로 처리하고, 실시간 응답은 WebClient로 처리하며, 오래 걸리는 작업은 메시지 큐로 분리하는 것이 좋다.


마무리

Spring Boot와 FastAPI를 함께 사용할 때 중요한 것은 무조건 복잡한 기술을 선택하는 것이 아니다.

가장 먼저 생각해야 할 것은 작업의 성격이다.

간단히 정리하면 다음과 같다.

구분추천 방식
일반 요청/응답REST API + RestClient
AI 실시간 응답WebClient
호출 API 정리HTTP Interface
오래 걸리는 AI 작업RabbitMQ
대량 이벤트 처리Kafka
기존 Feign 프로젝트OpenFeign 유지 가능

결국 처음에는 RestClient로 시작하는 것이 가장 현실적이다.

이후 프로젝트가 커지면서 AI 응답을 실시간으로 보여줘야 한다면 WebClient를 도입하고, 오래 걸리는 AI 작업이 많아지면 RabbitMQ 같은 메시지 큐를 추가하면 된다.

즉, Spring Boot와 FastAPI 통신 구조는 다음 흐름으로 발전시키면 된다.

RestClient 기반 REST 통신
→ WebClient 기반 스트리밍
→ RabbitMQ 기반 비동기 작업 처리

이렇게 설계하면 Spring Boot는 서비스의 중심 역할을 유지하고, FastAPI는 AI 기능에 집중할 수 있다.

결과적으로 각 서버의 책임이 명확해지고, 유지보수와 확장도 쉬워진다.


참고자료

Spring Boot / Spring Framework

FastAPI

Message Queue

WebClient에 대한 이해와 적용

· 약 11분
dev-burnern
Developer

Spring Boot에서 외부 API를 호출할 때 가장 먼저 떠올릴 수 있는 방식은 RestClient이다.

요청을 보내고 응답을 받은 뒤 다음 로직을 실행하는 단순한 구조라면 RestClient만으로도 충분하다.

하지만 AI 기능처럼 응답 시간이 길거나, 여러 외부 API를 동시에 호출하거나, 답변을 조금씩 스트리밍해야 하는 상황이라면 이야기가 달라진다.

이때 고려할 수 있는 HTTP Client가 WebClient이다.

이 글에서는 WebClient가 무엇인지, 언제 필요한지, Spring Boot에서 FastAPI를 호출할 때 어떻게 적용할 수 있는지 정리한다.


1. WebClient란 무엇인가

WebClient는 Spring WebFlux에서 제공하는 HTTP Client이다.

Spring 공식 문서에서는 WebClient를 HTTP 요청을 수행하기 위한 논블로킹, 리액티브 클라이언트로 설명한다.

간단히 말하면 다음과 같다.

WebClient는 Spring Boot에서 외부 API를 비동기, 논블로킹 방식으로 호출할 수 있게 해주는 HTTP Client이다.

예를 들어 Spring Boot가 FastAPI 서버에 AI 질문을 보내는 상황을 생각해 보자.

Spring Boot

FastAPI

AI 응답 생성

Spring Boot가 응답 수신

일반적인 요청/응답이라면 RestClient로도 충분하다.

하지만 다음과 같은 상황이라면 WebClient가 더 잘 맞는다.

AI 응답이 오래 걸리는 경우
AI 응답을 실시간으로 스트리밍하는 경우
여러 외부 API를 동시에 호출해야 하는 경우
요청 처리 스레드를 오래 붙잡고 싶지 않은 경우

2. RestClient와 WebClient의 차이

RestClientWebClient의 가장 큰 차이는 처리 방식이다.

구분RestClientWebClient
처리 방식동기식비동기, 논블로킹
반환 타입일반 객체Mono, Flux
사용 난이도쉬움상대적으로 어려움
적합한 상황짧은 요청/응답스트리밍, 동시 호출, 긴 요청
대표 예시일반 REST API 호출AI 스트리밍, 외부 API 조합

RestClient는 요청을 보내면 응답이 돌아올 때까지 현재 흐름이 기다린다.

요청 보냄
→ 응답을 기다림
→ 응답을 받은 뒤 다음 코드 실행

반면 WebClient는 응답을 기다리는 동안 호출 스레드를 계속 붙잡아 두지 않는 방식으로 동작할 수 있다.

요청 보냄
→ 응답이 준비되면 후속 처리 실행
→ 스트리밍 응답이면 조각 단위로 처리 가능

그래서 WebClient는 단순히 “새로운 HTTP Client”라기보다, 리액티브 흐름을 다룰 수 있는 HTTP Client에 가깝다.


3. Mono와 Flux 이해하기

WebClient를 사용하면 MonoFlux를 자주 보게 된다.

처음 보면 어렵게 느껴질 수 있지만, 기본 개념은 단순하다.

타입의미예시
Mono<T>0개 또는 1개의 결과AI 답변 하나
Flux<T>0개 이상의 여러 결과스트리밍 응답 조각

예를 들어 FastAPI에서 AI 답변을 한 번에 반환한다면 Mono가 어울린다.

Mono<AiAnswerResponse>

반대로 FastAPI가 AI 답변을 토큰이나 문장 단위로 계속 흘려보낸다면 Flux가 어울린다.

Flux<String>

정리하면 다음과 같다.

한 번에 끝나는 응답은 Mono, 여러 조각으로 이어지는 응답은 Flux로 생각하면 된다.


4. 의존성 추가

Spring Boot에서 WebClient를 사용하려면 보통 spring-boot-starter-webflux 의존성을 추가한다.

Maven은 다음과 같다.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Gradle은 다음과 같다.

implementation 'org.springframework.boot:spring-boot-starter-webflux'

중요한 점은 WebClient를 사용한다고 해서 전체 애플리케이션을 반드시 WebFlux 방식으로 바꿔야 하는 것은 아니라는 점이다.

Spring MVC 기반 프로젝트에서도 외부 API 호출용으로 WebClient를 사용할 수 있다.

다만 반환 타입이 Mono, Flux가 되기 때문에 서비스와 컨트롤러에서 이 흐름을 어떻게 다룰지 결정해야 한다.


5. WebClient 기본 설정

FastAPI 서버를 호출하는 WebClient를 Bean으로 등록해 보자.

package com.example.ai;

import java.time.Duration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;

import reactor.netty.http.client.HttpClient;

@Configuration
public class FastApiWebClientConfig {

@Bean
public WebClient fastApiWebClient(WebClient.Builder builder) {
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(30));

return builder
.baseUrl("http://localhost:8000")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}

여기서 중요한 설정은 baseUrl과 timeout이다.

설정의미
baseUrlFastAPI 서버 기본 주소
responseTimeout응답 대기 시간 제한
clientConnector실제 HTTP Client 연결 설정

AI API는 일반 API보다 응답 시간이 길어질 수 있다.

그래서 timeout을 명시하지 않으면 장애 상황에서 요청이 너무 오래 붙잡힐 수 있다.


6. FastAPI 일반 응답 호출하기

먼저 AI 답변을 한 번에 받는 API를 호출해 보자.

요청 DTO는 다음과 같이 만들 수 있다.

package com.example.ai;

public record AiAnswerRequest(
String question
) {
}

응답 DTO는 다음과 같다.

package com.example.ai;

import java.util.List;

public record AiAnswerResponse(
String answer,
List<String> references
) {
}

FastAPI 호출 Client는 다음과 같이 작성할 수 있다.

package com.example.ai;

import java.time.Duration;

import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import reactor.core.publisher.Mono;

@Component
public class FastApiAiClient {

private final WebClient fastApiWebClient;

public FastApiAiClient(WebClient fastApiWebClient) {
this.fastApiWebClient = fastApiWebClient;
}

public Mono<AiAnswerResponse> ask(AiAnswerRequest request) {
return fastApiWebClient.post()
.uri("/api/v1/ai/ask")
.bodyValue(request)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, response ->
Mono.error(new IllegalArgumentException("FastAPI 요청 값이 올바르지 않습니다.")))
.onStatus(HttpStatusCode::is5xxServerError, response ->
Mono.error(new IllegalStateException("FastAPI 서버 처리 중 오류가 발생했습니다.")))
.bodyToMono(AiAnswerResponse.class)
.timeout(Duration.ofSeconds(30));
}
}

핵심은 다음 부분이다.

.retrieve()
.bodyToMono(AiAnswerResponse.class)

retrieve()는 응답을 어떻게 꺼낼지 선언하는 메서드이고, bodyToMono()는 응답 body를 하나의 객체로 변환한다.

즉, 이 코드는 다음 의미를 가진다.

FastAPI에 POST 요청을 보내고, 응답 body를 AiAnswerResponse 하나로 변환한다.


7. Service에서 사용하는 방식

Service에서는 Mono를 그대로 반환할 수 있다.

package com.example.ai;

import org.springframework.stereotype.Service;

import reactor.core.publisher.Mono;

@Service
public class AiService {

private final FastApiAiClient fastApiAiClient;

public AiService(FastApiAiClient fastApiAiClient) {
this.fastApiAiClient = fastApiAiClient;
}

public Mono<AiAnswerResponse> ask(String question) {
return fastApiAiClient.ask(new AiAnswerRequest(question));
}
}

컨트롤러도 Mono를 반환하도록 만들 수 있다.

package com.example.ai;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/ai")
public class AiController {

private final AiService aiService;

public AiController(AiService aiService) {
this.aiService = aiService;
}

@PostMapping("/ask")
public Mono<AiAnswerResponse> ask(@RequestBody AiAnswerRequest request) {
return aiService.ask(request.question());
}
}

이 구조에서는 Spring Boot가 FastAPI 응답을 받은 뒤 사용자에게 결과를 반환한다.

Frontend

Spring Boot Controller

AiService

FastApiAiClient

FastAPI

8. block()은 언제 써야 할까

WebClient를 처음 사용할 때 자주 보이는 코드가 block()이다.

AiAnswerResponse response = fastApiAiClient.ask(request).block();

block()MonoFlux의 결과가 나올 때까지 현재 스레드를 기다리게 만든다.

즉, WebClient를 사용해 놓고 다시 동기식 흐름으로 바꾸는 것이다.

물론 모든 block()이 잘못된 것은 아니다.

예를 들어 배치 작업, 커맨드라인 실행, 테스트 코드처럼 동기식으로 결과가 반드시 필요한 경계에서는 사용할 수 있다.

하지만 웹 요청 처리 흐름 안에서 습관적으로 block()을 사용하면 WebClient의 장점이 줄어든다.

정리하면 다음과 같다.

웹 요청 처리 흐름에서는 가능하면 MonoFlux를 유지하고, 정말 필요한 경계에서만 block()을 사용한다.


9. AI 스트리밍 응답 처리

WebClient가 특히 유용한 상황은 스트리밍이다.

예를 들어 FastAPI가 AI 응답을 한 번에 반환하지 않고, 토큰이나 문장 단위로 계속 보내는 구조를 생각해 보자.

FastAPI 응답:





이런 응답은 하나의 객체가 아니라 여러 조각의 흐름이다.

그래서 Mono가 아니라 Flux로 받는 것이 자연스럽다.

Spring Boot Client 코드는 다음과 같이 작성할 수 있다.

package com.example.ai;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import reactor.core.publisher.Flux;

@Component
public class FastApiStreamingClient {

private final WebClient fastApiWebClient;

public FastApiStreamingClient(WebClient fastApiWebClient) {
this.fastApiWebClient = fastApiWebClient;
}

public Flux<String> stream(AiAnswerRequest request) {
return fastApiWebClient.post()
.uri("/api/v1/ai/stream")
.accept(MediaType.TEXT_EVENT_STREAM)
.bodyValue(request)
.retrieve()
.bodyToFlux(String.class);
}
}

컨트롤러도 text/event-stream으로 반환할 수 있다.

package com.example.ai;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/ai")
public class AiStreamingController {

private final FastApiStreamingClient streamingClient;

public AiStreamingController(FastApiStreamingClient streamingClient) {
this.streamingClient = streamingClient;
}

@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestBody AiAnswerRequest request) {
return streamingClient.stream(request);
}
}

이렇게 하면 흐름은 다음과 같다.

Frontend

Spring Boot

FastAPI

AI 응답 조각을 Flux로 수신

Frontend에 스트리밍 전달

10. FastAPI 스트리밍 예시

FastAPI에서는 StreamingResponse를 사용해서 간단한 스트리밍 API를 만들 수 있다.

import asyncio

from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import StreamingResponse

app = FastAPI()


class AiAnswerRequest(BaseModel):
question: str


@app.post("/api/v1/ai/stream")
async def stream_ai(request: AiAnswerRequest):
async def generate():
chunks = [
"질문을 분석했습니다.",
"관련 문서를 검색했습니다.",
f"'{request.question}'에 대한 답변을 생성합니다.",
]

for chunk in chunks:
yield f"data: {chunk}\n\n"
await asyncio.sleep(0.5)

return StreamingResponse(generate(), media_type="text/event-stream")

실제 LLM을 붙이면 chunks 배열 대신 LLM에서 나오는 토큰이나 문장 조각을 yield하면 된다.

이 구조에서는 FastAPI가 응답을 끝까지 만든 뒤 한 번에 보내지 않는다.

생성되는 대로 조금씩 Spring Boot에 전달하고, Spring Boot는 다시 Frontend로 전달할 수 있다.


11. 여러 API를 동시에 호출하기

WebClient는 여러 비동기 호출을 조합할 때도 유용하다.

예를 들어 FastAPI에서 RAG 검색 결과와 AI 요약 결과를 각각 받아와야 한다고 하자.

RAG 검색 API
AI 요약 API

이 둘이 서로 의존하지 않는다면 순서대로 기다릴 필요가 없다.

Mono.zip()을 사용하면 두 결과를 조합할 수 있다.

public Mono<AiCombinedResponse> askWithSearch(AiAnswerRequest request) {
Mono<RagSearchResponse> search = fastApiWebClient.post()
.uri("/api/v1/ai/rag/search")
.bodyValue(request)
.retrieve()
.bodyToMono(RagSearchResponse.class);

Mono<AiAnswerResponse> answer = fastApiWebClient.post()
.uri("/api/v1/ai/ask")
.bodyValue(request)
.retrieve()
.bodyToMono(AiAnswerResponse.class);

return Mono.zip(search, answer)
.map(tuple -> new AiCombinedResponse(tuple.getT1(), tuple.getT2()));
}

이런 구조는 외부 API 호출이 많아질수록 장점이 커진다.

다만 무조건 동시에 많이 호출하는 것이 좋은 것은 아니다.

FastAPI 서버, LLM API, Vector DB가 감당할 수 있는 동시 요청 수를 고려해야 한다.


12. timeout과 retry

외부 API를 호출할 때 timeout은 필수에 가깝다.

AI API는 다음 이유로 응답이 늦어질 수 있다.

LLM 응답 지연
Vector DB 검색 지연
문서 분석 작업 지연
FastAPI Worker 부하
네트워크 지연

그래서 요청마다 timeout을 둘 수 있다.

return fastApiWebClient.post()
.uri("/api/v1/ai/ask")
.bodyValue(request)
.retrieve()
.bodyToMono(AiAnswerResponse.class)
.timeout(Duration.ofSeconds(30));

일시적인 네트워크 오류라면 retry도 고려할 수 있다.

return fastApiWebClient.post()
.uri("/api/v1/ai/ask")
.bodyValue(request)
.retrieve()
.bodyToMono(AiAnswerResponse.class)
.timeout(Duration.ofSeconds(30))
.retry(1);

다만 retry는 조심해야 한다.

AI 요청은 비용이 발생하거나 같은 작업이 중복 실행될 수 있기 때문이다.

예를 들어 결제, 데이터 변경, 문서 임베딩 생성 같은 요청은 단순 retry가 위험할 수 있다.

정리하면 다음과 같다.

조회성 요청은 제한적인 retry를 고려할 수 있지만, 비용이 크거나 상태를 변경하는 요청은 멱등성과 중복 실행을 먼저 설계해야 한다.


13. 에러 처리는 어디까지 해야 할까

WebClientretrieve()는 기본적으로 4xx, 5xx 응답을 에러로 처리한다.

하지만 서비스에서 사용자가 이해할 수 있는 에러로 바꾸려면 onStatus()를 사용할 수 있다.

return fastApiWebClient.post()
.uri("/api/v1/ai/ask")
.bodyValue(request)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, response ->
Mono.error(new IllegalArgumentException("AI 요청 형식이 올바르지 않습니다.")))
.onStatus(HttpStatusCode::is5xxServerError, response ->
Mono.error(new IllegalStateException("AI 서버가 일시적으로 응답하지 않습니다.")))
.bodyToMono(AiAnswerResponse.class);

실무에서는 보통 다음을 구분한다.

구분예시처리 방향
4xx질문 값 누락, 잘못된 요청사용자 요청 오류로 처리
5xxFastAPI 내부 오류서버 오류 또는 재시도 가능 오류로 처리
timeout응답 지연사용자에게 지연 안내
연결 실패FastAPI 서버 다운장애 또는 fallback 처리

에러 처리는 복잡하게 시작할 필요는 없다.

처음에는 timeout, 4xx, 5xx만 명확히 나눠도 충분하다.


14. WebClient를 적용할 때 주의할 점

WebClient는 강력하지만 무조건 도입할 필요는 없다.

단순한 요청/응답만 있는 프로젝트에서 모든 외부 호출을 리액티브 방식으로 바꾸면 오히려 코드가 어려워질 수 있다.

적용할 때는 다음 기준을 먼저 확인하는 것이 좋다.

질문판단
응답을 스트리밍해야 하는가?그렇다면 WebClient가 적합
여러 API를 동시에 호출해야 하는가?WebClient 고려
요청 시간이 길고 스레드를 붙잡기 싫은가?WebClient 고려
단순 CRUD API 호출인가?RestClient로 충분
팀이 Reactor 흐름에 익숙한가?학습 비용 고려

특히 Mono, Flux를 중간에 계속 block()으로 끊어 버린다면 WebClient를 쓰는 이점이 줄어든다.

WebClient를 도입한다면 가능한 한 흐름 끝까지 리액티브 타입을 유지하는 편이 좋다.


15. Spring Boot와 FastAPI 구조에서의 선택 기준

Spring Boot와 FastAPI를 함께 쓴다면 다음처럼 나눠 생각할 수 있다.

상황추천
일반 AI 질문/응답RestClient 또는 WebClient
짧고 단순한 RAG 검색RestClient
AI 답변 실시간 출력WebClient + Flux
여러 AI API 동시 호출WebClient
PDF 분석, 임베딩 생성 등 오래 걸리는 작업RabbitMQ 같은 메시지 큐
작업 상태를 나중에 조회메시지 큐 + DB/Redis

즉, WebClient는 메시지 큐를 대체하는 기술이 아니다.

실시간 HTTP 응답 처리와 스트리밍에 강한 HTTP Client이다.

작업 자체가 몇 분 이상 걸리거나 백그라운드 처리가 필요하다면 메시지 큐가 더 적합하다.


마무리

WebClient는 Spring Boot에서 외부 API를 비동기, 논블로킹 방식으로 호출할 수 있게 해주는 HTTP Client이다.

특히 FastAPI와 AI 기능을 연동할 때 다음 상황에서 유용하다.

AI 응답 스트리밍
긴 외부 API 요청
여러 API 동시 호출
논블로킹 처리 흐름 유지

하지만 모든 상황에서 WebClient가 정답은 아니다.

짧고 단순한 요청/응답이라면 RestClient가 더 읽기 쉽고 유지보수하기 편할 수 있다.

정리하면 다음과 같다.

상황선택
단순 요청/응답RestClient
비동기 처리WebClient
스트리밍 응답WebClient + Flux
오래 걸리는 백그라운드 작업메시지 큐

결국 중요한 것은 기술 이름이 아니라 작업의 성격이다.

Spring Boot와 FastAPI를 함께 사용할 때는 처음부터 복잡한 구조로 시작하기보다, 단순 요청은 RestClient로 처리하고 스트리밍이나 동시 호출이 필요해지는 지점에서 WebClient를 도입하는 것이 현실적이다.

참고자료

Spring Framework

Reactor