본문으로 건너뛰기

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

모든 태그 보기

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

RAG란 무엇인가

· 약 11분
dev-burnern
Developer

처음 ChatGPT 같은 AI를 사용하면 보통 이렇게 생각하기 쉽다.

AI는 이미 모든 지식을 알고 있으니 질문하면 바로 답해준다.

하지만 실제로는 그렇지 않다.
AI 모델은 학습된 데이터를 바탕으로 답변을 생성하지만, 최신 정보, 회사 내부 문서, 개인 프로젝트 문서, 특정 서비스의 정책 같은 내용은 모를 수 있다.

예를 들어 다음과 같은 질문을 했다고 하자.

우리 회사의 휴가 정책 알려줘내 프로젝트 API 명세 기준으로 로그인 흐름 설명해줘이번 달 업데이트된 서비스 약관 요약해줘

일반 LLM은 이런 질문에 정확히 답하기 어렵다.
왜냐하면 모델이 학습할 때 그 문서를 본 적이 없을 수 있기 때문이다.

이 문제를 해결하기 위해 등장한 방식이 RAG이다.


1. RAG의 기본 의미

RAG는 Retrieval-Augmented Generation의 약자이다.

한국어로는 보통 검색 증강 생성이라고 부른다.

단어를 나눠보면 다음과 같다.

단어의미
Retrieval필요한 정보를 검색함
Augmented검색한 정보로 보강함
GenerationAI가 답변을 생성함

즉, RAG는 쉽게 말하면 다음과 같다.

AI가 바로 대답하는 것이 아니라, 먼저 관련 문서를 검색한 뒤 그 내용을 참고해서 답변하는 방식

AWS는 RAG를 LLM이 응답을 생성하기 전에 학습 데이터 밖의 신뢰할 수 있는 지식 자료를 참조하도록 하는 방식으로 설명한다.


2. RAG가 필요한 이유

LLM은 강력하지만 한계가 있다.

대표적인 한계는 다음과 같다.

최신 정보를 모를 수 있다.회사 내부 문서를 모른다.특정 프로젝트의 정책이나 구조를 모른다.모르는 내용도 그럴듯하게 지어낼 수 있다.답변의 근거를 확인하기 어렵다.

예를 들어 AI에게 다음과 같이 물어봤다고 하자.

우리 코빕 프로젝트의 템플릿 구매 API 흐름을 설명해줘

AI가 코빕 프로젝트의 ERD, API 명세서, 정책 문서를 가지고 있지 않다면 정확히 답하기 어렵다.
그런데도 일반적인 쇼핑몰 구매 흐름을 바탕으로 그럴듯하게 답할 수 있다.

이게 바로 문제다.

정확한 답변이 필요한 상황에서는 “그럴듯한 답변”보다 근거 있는 답변이 중요하다.

RAG는 이런 상황에서 외부 문서를 검색해 LLM에게 함께 제공함으로써 더 관련성 있고 정확한 답변을 만들 수 있게 한다. IBM도 RAG를 외부 지식 베이스와 연결해 AI 모델의 성능을 높이는 아키텍처로 설명한다.

정리하면 RAG가 필요한 이유는 다음과 같다.

AI가 모르는 정보를 외부 문서에서 찾아서, 근거를 기반으로 답변하게 만들기 위해서


3. RAG의 동작 흐름

RAG의 기본 흐름은 어렵지 않다.

문서 준비→ 문서 쪼개기→ 임베딩 생성→ 벡터 DB 저장→ 사용자 질문 입력→ 관련 문서 검색→ 검색 결과를 프롬프트에 추가→ LLM이 답변 생성

조금 더 쉽게 표현하면 다음과 같다.

1. 미리 문서를 저장해둔다.2. 사용자가 질문한다.3. 질문과 관련 있는 문서를 찾는다.4. 찾은 문서를 AI에게 같이 준다.5. AI가 문서를 참고해서 답변한다.

AWS Prescriptive Guidance에서도 RAG 과정을 내부 문서를 임베딩해 벡터 데이터베이스에 저장하고, 사용자의 질문이 들어오면 관련 데이터를 검색해 프롬프트에 추가한 뒤 LLM이 답변을 생성하는 흐름으로 설명한다.


4. RAG를 도서관에 비유하면

RAG를 도서관에 비유하면 이해하기 쉽다.

일반 LLM은 기억력이 좋은 사람과 비슷하다.

하지만 그 사람이 모든 책의 최신 내용을 외우고 있는 것은 아니다.
특정 회사의 내부 문서나 최근에 바뀐 정책도 모를 수 있다.

RAG는 이 사람에게 도서관 검색 시스템을 붙여주는 것과 비슷하다.

사용자 질문: "우리 회사 연차 정책 알려줘"1. 질문을 받는다.2. 회사 규정 문서에서 연차 관련 내용을 찾는다.3. 찾은 문서를 읽는다.4. 그 내용을 바탕으로 답변한다.

즉, RAG는 AI가 혼자 기억에 의존하지 않게 만든다.

정리하면 다음과 같다.

RAG는 AI에게 검색 능력을 붙여주는 방식이다.


5. RAG의 핵심 구성 요소

RAG 시스템은 보통 다음 요소들로 구성된다.

구성 요소역할
문서 데이터PDF, Markdown, Notion, API 문서, DB 데이터 등
Loader문서를 불러오는 역할
Splitter긴 문서를 작은 단위로 나누는 역할
Embedding Model텍스트를 숫자 벡터로 바꾸는 역할
Vector DB임베딩된 문서를 저장하고 검색하는 저장소
Retriever질문과 관련 있는 문서를 찾아오는 역할
Prompt검색 결과와 사용자 질문을 LLM에게 전달하는 형식
LLM최종 답변을 생성하는 모델

LangChain 문서에서도 RAG의 검색 파이프라인을 문서 로더, 텍스트 분할기, 임베딩 모델, 벡터 저장소, 검색기 같은 모듈로 설명한다.

정리하면 RAG는 단순히 AI API를 호출하는 구조가 아니다.

문서를 저장하고, 검색하고, 검색 결과를 AI에게 잘 전달하는 전체 구조가 RAG이다.


6. 임베딩이란

RAG를 이해하려면 임베딩을 알아야 한다.

임베딩은 텍스트를 숫자 배열로 바꾸는 작업이다.

예를 들어 다음 문장들이 있다고 하자.

JWT는 인증에 사용된다.Access Token은 로그인 상태를 확인할 때 사용된다.강아지는 귀엽다.

사람이 보기에는 첫 번째와 두 번째 문장이 비슷한 주제라는 것을 알 수 있다.
하지만 컴퓨터는 문장을 그대로 이해하지 못한다.

그래서 문장을 숫자로 바꾼다.

"JWT는 인증에 사용된다."→ [0.12, -0.33, 0.87, ...]"Access Token은 로그인 상태를 확인할 때 사용된다."→ [0.15, -0.29, 0.81, ...]

의미가 비슷한 문장은 벡터 공간에서 가까운 위치에 배치된다.
그래서 사용자가 “로그인 인증 방식 알려줘”라고 물으면 JWT나 Access Token 관련 문서를 찾을 수 있다.

정리하면 임베딩은 다음과 같다.

문장의 의미를 비교할 수 있도록 텍스트를 숫자 벡터로 바꾸는 작업


7. 벡터 DB란

벡터 DB는 임베딩된 문서를 저장하고 검색하는 데이터베이스이다.

일반 데이터베이스는 보통 정확한 값을 찾는 데 강하다.

예를 들어 다음과 같은 검색이다.

SELECT * FROM users WHERE email = 'test@example.com';

하지만 RAG에서는 단순히 같은 단어가 들어간 문서만 찾으면 부족하다.

예를 들어 사용자가 이렇게 물어볼 수 있다.

로그인 유지 방식 설명해줘

문서에는 “Refresh Token”, “세션 유지”, “JWT 재발급”이라는 표현이 있을 수 있다.
단어가 완전히 같지 않아도 의미가 비슷한 문서를 찾아야 한다.

이때 벡터 DB가 사용된다.

벡터 DB는 질문 벡터와 문서 벡터를 비교해서 의미적으로 가까운 문서를 찾는다.

정리하면 벡터 DB는 다음과 같다.

의미가 비슷한 문서를 빠르게 찾기 위해 벡터를 저장하고 검색하는 데이터베이스


8. RAG와 일반 검색의 차이

RAG는 단순 검색과 다르다.

일반 검색은 관련 문서를 찾아서 보여주는 것에 가깝다.

사용자 질문→ 관련 문서 검색→ 검색 결과 목록 제공

RAG는 검색한 문서를 바탕으로 AI가 답변까지 생성한다.

사용자 질문→ 관련 문서 검색→ 검색 결과를 AI에게 전달→ AI가 문서를 참고해서 답변 생성

차이를 표로 정리하면 다음과 같다.

구분일반 검색RAG
목적문서 찾기문서를 참고한 답변 생성
결과검색 결과 목록자연어 답변
AI 사용필수 아님LLM 사용
근거 문서사용자가 직접 읽음AI가 참고해서 요약
예시구글 검색, 문서 검색문서 기반 챗봇, 사내 지식 QA

정리하면 다음과 같다.

일반 검색은 문서를 찾아주는 것이고, RAG는 찾은 문서를 바탕으로 답변까지 만들어주는 것이다.


9. RAG와 파인튜닝의 차이

RAG를 처음 배우면 파인튜닝과 헷갈릴 수 있다.

둘 다 AI를 특정 목적에 맞게 개선하는 방법이기 때문이다.

하지만 방향이 다르다.

구분RAG파인튜닝
방식외부 문서를 검색해서 참고모델을 추가 학습
데이터 반영문서만 업데이트하면 됨다시 학습이 필요할 수 있음
비용상대적으로 낮음상대적으로 높을 수 있음
적합한 경우최신 정보, 내부 문서, 정책 QA말투, 형식, 특정 작업 패턴 학습
예시사내 문서 챗봇고객센터 답변 스타일 학습

예를 들어 회사의 휴가 정책이 바뀌었다고 하자.

RAG는 휴가 정책 문서만 수정하고 다시 색인하면 된다.
하지만 파인튜닝은 모델에 새 정책을 다시 학습시켜야 할 수 있다.

AWS도 RAG를 사용하면 모델을 재학습하지 않고 조직이나 도메인별 정보를 더 비용 효율적으로 도입할 수 있다고 설명한다.

정리하면 다음과 같다.

지식을 넣고 싶으면 RAG, 행동 방식이나 답변 스타일을 바꾸고 싶으면 파인튜닝에 가깝다.


10. RAG 구현 예시

예를 들어 개발 문서 기반 Q&A 챗봇을 만든다고 하자.

사용자가 다음과 같이 질문한다.

우리 프로젝트에서 Refresh Token은 어디에 저장돼?

RAG가 없다면 LLM은 일반적인 답변을 할 수 있다.

Refresh Token은 보통 Redis나 DB에 저장합니다.

하지만 이 답변은 일반론이다.
우리 프로젝트에서 실제로 Redis를 쓰는지, DB를 쓰는지는 모른다.

RAG를 적용하면 다음과 같은 흐름이 된다.

1. API 명세서, ERD, README, 보안 정책 문서를 저장한다.2. 사용자가 Refresh Token 저장 위치를 질문한다.3. RAG가 관련 문서를 검색한다.4. 검색된 문서에 "Refresh Token은 Redis에 저장한다"는 내용이 있다.5. LLM이 해당 내용을 참고해서 답변한다.

그러면 답변은 이렇게 바뀔 수 있다.

우리 프로젝트에서는 Refresh Token을 Redis에 저장합니다.이유는 토큰 만료 관리와 로그아웃 시 즉시 무효화 처리를 쉽게 하기 위해서입니다.관련 내용은 인증 설계 문서의 Token Storage 섹션에 정의되어 있습니다.

정리하면 다음과 같다.

RAG는 일반적인 답변이 아니라, 내가 가진 문서를 기준으로 답변하게 만든다.


11. RAG를 사용할 수 있는 분야

RAG는 여러 분야에서 사용할 수 있다.

사내 문서 검색 챗봇개발 문서 Q&A법률 문서 요약의료 가이드 검색논문 기반 질의응답고객센터 자동 답변교육용 학습 도우미프로젝트 문서 분석API 명세 기반 챗봇

예를 들어 개발자 입장에서는 다음과 같은 서비스를 만들 수 있다.

README 기반 프로젝트 설명 챗봇API 명세서 기반 백엔드 Q&AERD 기반 테이블 관계 설명 챗봇장애 로그 기반 원인 분석 도우미기술 블로그 기반 검색형 AI

특히 문서가 많고, 사용자가 매번 직접 찾아보기 어려운 서비스에 RAG가 잘 맞는다.

정리하면 다음과 같다.

RAG는 문서가 많고, 그 문서를 바탕으로 정확히 답변해야 하는 서비스에 적합하다.


12. RAG의 한계

RAG를 쓰면 모든 문제가 해결되는 것은 아니다.

RAG에도 한계가 있다.

문서를 잘못 검색하면 답변도 틀릴 수 있다.문서 자체가 오래되었으면 오래된 답변을 한다.문서 쪼개기를 잘못하면 중요한 맥락이 끊긴다.검색 결과가 너무 많으면 LLM이 핵심을 놓칠 수 있다.권한 처리를 잘못하면 민감한 문서가 노출될 수 있다.

예를 들어 사용자에게 권한이 없는 문서까지 검색 결과에 포함되면 보안 문제가 생길 수 있다.

또 문서가 다음처럼 나뉘어 있다고 하자.

1번 조각: Refresh Token은 Redis에 저장한다.2번 조각: 단, 운영 환경에서는 암호화 저장 정책을 적용한다.

만약 1번 조각만 검색되고 2번 조각이 빠지면 답변이 불완전해질 수 있다.

정리하면 다음과 같다.

RAG의 품질은 모델만이 아니라 문서 품질, 검색 품질, 권한 처리, 프롬프트 설계에 크게 좌우된다.


13. 좋은 RAG를 만들기 위해 신경 쓸 점

RAG를 잘 만들기 위해서는 단순히 벡터 DB를 붙이는 것만으로는 부족하다.

중요한 요소는 다음과 같다.

항목설명
문서 품질오래되거나 중복된 문서를 정리해야 함
청킹 전략문서를 적절한 크기로 나눠야 함
메타데이터문서 제목, 작성일, 카테고리, 권한 정보 등을 함께 저장
검색 품질질문과 관련 있는 문서를 잘 찾아야 함
재랭킹검색 결과 중 더 중요한 문서를 다시 정렬
프롬프트 설계검색된 문서를 근거로 답변하도록 지시
출처 표시답변이 어떤 문서를 기반으로 했는지 보여줌
권한 관리사용자별로 접근 가능한 문서만 검색
평가 시스템답변 정확도와 검색 품질을 지속적으로 측정

특히 실무에서는 “답변이 그럴듯한가”보다 다음이 더 중요하다.

근거 문서를 제대로 찾았는가?문서에 없는 내용을 지어내지 않았는가?최신 문서를 참고했는가?사용자 권한에 맞는 문서만 사용했는가?

정리하면 다음과 같다.

RAG는 AI 기능이 아니라 검색 시스템과 문서 관리 시스템까지 포함한 구조로 봐야 한다.


14. 기본 RAG와 발전된 RAG

처음에는 보통 단순한 RAG 구조로 시작한다.

질문→ 문서 검색→ LLM 답변

하지만 서비스가 복잡해지면 다양한 구조가 필요해진다.

LangChain 문서에서는 RAG 구조를 2-Step RAG, Agentic RAG, Hybrid RAG 등으로 나누어 설명한다.

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

구분의미
기본 RAG질문이 들어오면 관련 문서를 검색한 뒤 답변
2-Step RAG검색 후 생성 흐름이 고정된 단순하고 예측 가능한 구조
Agentic RAGAI가 필요할 때 도구나 검색을 스스로 선택해서 사용
Hybrid RAG검색, 검증, 재검색 등을 섞어 답변 품질을 높이는 구조

처음부터 복잡한 Agentic RAG를 만들 필요는 없다.

대부분의 프로젝트는 다음 구조로 시작해도 충분하다.

문서 업로드→ 청킹→ 임베딩→ 벡터 DB 저장→ 질문→ 유사 문서 검색→ LLM 답변→ 출처 표시

정리하면 다음과 같다.

처음에는 기본 RAG로 시작하고, 검색 품질과 답변 품질이 부족할 때 고급 구조를 붙이는 것이 좋다.


마무리

RAG는 AI가 모든 것을 외우게 만드는 기술이 아니다.

오히려 반대에 가깝다.

AI가 모르는 내용은 외부 문서에서 찾고, 그 문서를 근거로 답변하게 만드는 방식이다.

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

구분설명
RAG검색 증강 생성
목적외부 문서를 참고해 더 정확한 답변 생성
핵심 흐름검색 → 문서 제공 → 답변 생성
주요 구성문서, 임베딩, 벡터 DB, Retriever, LLM
장점최신 정보, 내부 문서, 근거 기반 답변에 유리
한계검색 품질, 문서 품질, 권한 관리가 중요
적합한 서비스사내 문서 챗봇, 개발 문서 QA, 학습 플랫폼, 고객센터

결국 RAG를 이해할 때 가장 중요한 것은 다음이다.

RAG는 AI에게 “기억”을 추가하는 것이 아니라, “필요한 자료를 찾아 읽고 답하는 구조”를 붙이는 것이다.

그래서 RAG를 잘 만들려면 단순히 LLM API를 잘 쓰는 것만으로는 부족하다.
좋은 문서 구조, 검색 품질, 권한 처리, 프롬프트 설계, 출처 표시까지 함께 설계해야 한다.

참고자료

RAG 기본 개념

RAG 동작 흐름 / 아키텍처

구현 관점 / 실무 적용

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 / 검색 도메인