본문으로 건너뛰기

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

모든 태그 보기

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

IT에서 말하는 도메인이란

· 약 8분
dev-burnern
Developer

처음에는 보통 google.com, naver.com 같은 인터넷 주소를 떠올린다. 하지만 백엔드, 데이터베이스, 네트워크, 보안, DDD, AI 분야로 넘어가면 도메인의 의미가 조금씩 달라진다.

도메인이라는 단어의 기본 뜻은 영역, 범위, 분야이다.
즉, IT에서 도메인은 문맥에 따라 “주소의 영역”, “업무의 영역”, “데이터 값의 범위”, “지식의 분야” 처럼 다르게 해석된다.

1. 인터넷 주소로서의 도메인

가장 흔하게 접하는 도메인은 웹사이트 주소이다.

예를 들어 다음과 같은 것들이 있다.

google.com
naver.com
dev-burnern.dev
github.com

사람은 숫자로 된 IP 주소보다 문자로 된 이름을 기억하기 쉽다.
그래서 142.250.xxx.xxx 같은 IP 주소 대신 google.com 같은 도메인 이름을 사용한다.

즉, 인터넷 주소로서의 도메인은 쉽게 말하면 다음과 같다.

사람이 기억하기 쉬운 웹사이트 이름

예를 들어 개인 블로그를 운영할 때 dev-burnern.dev 같은 도메인을 구매해서 Vercel, GitHub Pages, AWS 같은 서비스에 연결할 수 있다.


2. DNS에서의 도메인

도메인 이름만 있다고 해서 바로 웹사이트에 접속할 수 있는 것은 아니다.
브라우저는 결국 서버의 IP 주소를 알아야 한다.

이때 사용되는 시스템이 DNS이다.

DNS는 Domain Name System의 약자로, 도메인 이름을 IP 주소로 바꿔주는 시스템이다.

예를 들어 사용자가 브라우저에 다음 주소를 입력했다고 하자.

dev-burnern.dev

그러면 DNS는 이 도메인이 어느 서버를 가리키는지 찾아준다.

DNS에서 자주 등장하는 설정은 다음과 같다.

DNS 레코드의미
A Record도메인을 IPv4 주소에 연결
AAAA Record도메인을 IPv6 주소에 연결
CNAME도메인을 다른 도메인에 연결
MX이메일 서버 설정
TXT인증, 보안, 소유권 확인 등에 사용
NS도메인을 관리하는 네임서버 지정
예를 들어 Vercel에 개인 도메인을 연결할 때 A RecordCNAME을 설정하는 이유가 여기에 있다.

정리하면 DNS에서의 도메인은 다음과 같다.

도메인 이름이 실제 서버를 찾아갈 수 있도록 관리되는 이름 체계


3. 이메일 도메인

이메일에서도 도메인이라는 개념이 사용된다.

예를 들어 다음 이메일 주소를 보자.

user@gmail.com
admin@company.com
contact@dev-burnern.dev

여기서 @ 뒤에 있는 부분이 이메일 도메인이다.

gmail.com
company.com
dev-burnern.dev

이메일 도메인은 해당 이메일이 어느 메일 서버를 사용하는지 알려준다.

예를 들어 user@gmail.comgmail.com 도메인의 메일 시스템을 사용한다.
회사 이메일인 name@company.com은 해당 회사 도메인을 사용하는 이메일이다.

이메일 도메인을 제대로 사용하려면 DNS에서 MX Record 설정이 필요하다.
또한 스팸 방지와 보안을 위해 SPF, DKIM, DMARC 같은 TXT 레코드 설정도 함께 사용된다.

정리하면 이메일 도메인은 다음과 같다.

이메일 주소에서 메일을 주고받는 서버와 소속을 나타내는 영역


4. 네트워크 도메인

네트워크 분야에서도 도메인이라는 단어가 사용된다.

대표적으로 Windows Domain, Active Directory Domain 같은 개념이 있다.

회사나 학교에서는 여러 컴퓨터, 사용자 계정, 권한, 보안 정책을 한 번에 관리해야 한다.
이때 같은 정책으로 묶인 네트워크 영역을 도메인이라고 부른다.

예를 들어 회사 내부에서 다음과 같은 작업을 중앙에서 관리할 수 있다.

직원 계정 관리
컴퓨터 접근 권한 관리
공유 폴더 접근 제어
로그인 정책 설정
보안 정책 적용

즉, 네트워크 도메인은 단순한 웹사이트 주소가 아니라 조직 내부의 컴퓨터와 사용자를 관리하는 영역에 가깝다.

정리하면 네트워크 도메인은 다음과 같다.

같은 관리 정책과 인증 체계를 공유하는 네트워크 영역


5. 비즈니스/서비스 도메인

백엔드 개발에서 가장 중요한 도메인 개념 중 하나는 비즈니스 도메인이다.

비즈니스 도메인은 서비스가 다루는 업무 영역을 의미한다.

예를 들어 쇼핑몰 서비스를 만든다고 하면 주요 도메인은 다음과 같다.

회원
상품
장바구니
주문
결제
배송
리뷰
쿠폰

배달 앱이라면 도메인이 이렇게 나뉠 수 있다.

사용자
가게
메뉴
주문
결제
배달
리뷰
정산

여기서 도메인은 단순한 테이블 이름이나 클래스 이름이 아니다.
서비스가 실제로 해결해야 하는 업무 영역을 의미한다.

예를 들어 주문 도메인은 단순히 주문 데이터를 저장하는 것이 아니라 다음과 같은 규칙을 포함한다.

사용자는 상품을 주문할 수 있다.
결제가 완료되어야 주문이 확정된다.
주문 취소는 배송 시작 전까지만 가능하다.
쿠폰은 특정 조건에서만 사용할 수 있다.

정리하면 비즈니스/서비스 도메인은 다음과 같다.

서비스가 해결하려는 실제 업무 영역


6. DDD에서의 도메인

DDD는 Domain-Driven Design, 즉 도메인 주도 설계이다.

DDD에서 도메인은 소프트웨어가 해결하려는 핵심 문제 영역을 뜻한다.

예를 들어 쇼핑몰에서 정말 중요한 것은 단순 CRUD가 아니다.

주문은 언제 생성되는가?
결제 실패 시 주문 상태는 어떻게 되는가?
배송이 시작된 주문은 취소할 수 있는가?
환불은 어떤 조건에서 가능한가?

DDD는 이런 비즈니스 규칙을 중심으로 소프트웨어를 설계하는 방식이다.

DDD에서는 다음과 같은 개념들이 자주 등장한다.

개념의미
Entity식별자를 가진 도메인 객체
Value Object값 자체가 중요한 객체
Aggregate관련된 객체를 하나로 묶은 단위
Repository도메인 객체를 저장하고 조회하는 역할
Domain Service특정 Entity에 넣기 애매한 도메인 로직
Bounded Context도메인 의미가 일관되게 유지되는 경계
예를 들어 User라는 단어도 서비스마다 의미가 다를 수 있다.

쇼핑몰에서는 구매자일 수 있고, 관리자 시스템에서는 운영자일 수 있으며, 배송 시스템에서는 수령자일 수 있다.
DDD에서는 이런 의미가 섞이지 않도록 Bounded Context를 나눈다.

정리하면 DDD에서의 도메인은 다음과 같다.

소프트웨어가 해결해야 할 핵심 비즈니스 문제 영역


7. 데이터베이스에서의 도메인

데이터베이스에서도 도메인이라는 단어가 사용된다.

데이터베이스에서 도메인은 어떤 컬럼이 가질 수 있는 값의 범위를 의미한다.

예를 들어 age 컬럼이 있다고 하자.

age: 0 이상 150 이하의 정수

이때 age의 도메인은 0~150 사이의 정수라고 볼 수 있다.

다른 예시는 다음과 같다.

컬럼도메인
genderM, F
price0 이상의 숫자
email이메일 형식을 만족하는 문자열
order_statusREADY, PAID, SHIPPED, CANCELED
created_at날짜와 시간
데이터베이스에서 도메인을 명확히 정하면 잘못된 데이터가 들어오는 것을 막을 수 있다.

예를 들어 주문 상태가 다음 값만 가져야 한다고 하자.

READY
PAID
SHIPPED
CANCELED

그런데 실수로 HELLO, DONE, 123 같은 값이 들어오면 데이터의 신뢰성이 깨진다.

그래서 데이터베이스에서는 CHECK 제약조건, ENUM, NOT NULL, FOREIGN KEY 등을 사용해 값의 범위를 제한한다.

정리하면 데이터베이스에서의 도메인은 다음과 같다.

컬럼이나 속성이 가질 수 있는 값의 범위


8. 보안/웹 정책에서의 도메인

웹 보안에서도 도메인은 매우 중요하다.

대표적인 예시는 쿠키, CORS, Same-Origin Policy이다.

쿠키에서의 도메인

쿠키는 특정 도메인에 저장된다.

예를 들어 example.com에서 발급한 쿠키는 기본적으로 example.com에서 사용된다.

example.com
api.example.com
admin.example.com

쿠키 설정에 따라 하위 도메인에서도 쿠키를 공유할 수 있다.

예를 들어 .example.com으로 쿠키 도메인을 설정하면 다음 하위 도메인에서도 쿠키를 사용할 수 있다.

www.example.com
api.example.com
admin.example.com

CORS에서의 도메인

CORS는 서로 다른 출처 간의 요청을 제어하는 정책이다.

예를 들어 프론트엔드가 다음 주소에서 실행되고 있다고 하자.

https://frontend.com

백엔드 API는 다음 주소에 있다.

https://api.backend.com

이 둘은 서로 다른 출처이기 때문에 브라우저는 보안상 요청을 제한할 수 있다.
이때 백엔드에서 CORS 설정을 통해 허용할 도메인을 지정해야 한다.

정리하면 보안/웹 정책에서의 도메인은 다음과 같다.

쿠키, 인증, 요청 허용 범위 등을 결정하는 웹 보안의 기준 영역


9. AI/검색에서의 도메인

AI와 검색 분야에서도 도메인이라는 단어가 자주 사용된다.

이때 도메인은 특정 지식 분야나 전문 영역을 의미한다.

예를 들어 다음과 같은 표현을 볼 수 있다.

의료 도메인
법률 도메인
금융 도메인
개발 도메인
교육 도메인
커머스 도메인

AI 모델은 일반적인 질문에는 잘 답할 수 있어도 특정 전문 분야에서는 정확도가 떨어질 수 있다.

예를 들어 의료, 법률, 금융 같은 분야는 전문 용어와 규칙이 많다.
그래서 해당 분야의 데이터를 추가로 학습하거나, RAG를 사용해 관련 문서를 검색한 뒤 답변하도록 만든다.

예를 들어 개발 문서 검색 AI를 만든다면 다음과 같은 도메인을 다루게 된다.

프로그래밍 언어
프레임워크
API 문서
에러 로그
기술 블로그
공식 문서

이때 “개발 도메인에 특화된 AI”라고 표현할 수 있다.

정리하면 AI/검색에서의 도메인은 다음과 같다.

AI가 이해하거나 검색해야 하는 특정 지식 분야


마무리

도메인이라는 단어는 IT에서 매우 자주 사용되지만, 문맥에 따라 의미가 달라진다.

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

구분도메인의 의미
인터넷 주소사람이 기억하기 쉬운 웹사이트 이름
DNS도메인을 IP 주소와 연결하는 이름 체계
이메일이메일 주소에서 @ 뒤의 영역
네트워크같은 정책으로 관리되는 네트워크 영역
비즈니스/서비스서비스가 다루는 업무 영역
DDD소프트웨어가 해결해야 할 핵심 문제 영역
데이터베이스컬럼이 가질 수 있는 값의 범위
보안/웹 정책쿠키, 인증, 요청 허용 범위의 기준
AI/검색특정 지식 분야

결국 도메인을 이해할 때 가장 중요한 것은 문맥이다.

도메인 구매했다라고 하면 웹사이트 주소를 의미하고,
주문 도메인 설계하자라고 하면 비즈니스 로직 영역을 의미한다.
의료 도메인 AI라고 하면 의료 지식 분야에 특화된 AI를 뜻한다.

따라서 개발자는 도메인이라는 단어를 볼 때 단순히 “인터넷 주소”로만 이해하면 안 된다.
어떤 분야에서 사용되는지에 따라 의미를 구분해서 이해해야 한다.

참고자료

인터넷 주소 / DNS

이메일 도메인

네트워크 도메인

비즈니스 / 서비스 도메인

DDD에서의 도메인

데이터베이스 도메인

보안 / 웹 정책

AI / 검색 도메인