본문으로 건너뛰기

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

모든 태그 보기

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