RAG 이해하기

 

이 글은 특강 내용 + 공식 문서 + 현업 적용 관점을 합쳐서 내가 다시 찾아볼 수 있도록 정리하였습니다.

목차 Spring AI가 뭔지 먼저 감 잡기

1.핵심 구성요소 정리 (ChatClient, ChatModel, Advisor 외백엔드 계층 어디에 AI를 두는가

2.ChatClient 기본 사용법프롬프트 엔지니어링 레이어Structured Output

3. AI 응답을 DTO로 받기RAG 왜 필요한가, 언제 필요한가

4.Naive RAG 구현 흐름

5.QuestionAnswerAdvisor 동작 원리

6.Naive RAG 한계 & 운영 체크리스트

7.현업에서 어떻게 활용하는가

8.참고 아티클핵심 요약 & 내 프로젝트 적용 포인트

01 — Overview
Spring AI가 뭔지 먼저 감 잡기

Spring AI는 LLM을 Spring Boot 서비스 계층에 연결하기 위한 추상화 프레임워크다. 단순히 OpenAI API를 호출하는 래퍼가 아니라, ChatModel / EmbeddingModel / VectorStore / Advisor / Tool 같은 구성요소를 Spring의 DI 방식으로 조합할 수 있게 해준다.

비유하자면 Spring Data처럼 — JPA를 쓸 때 DB 벤더를 바꿔도 Repository 인터페이스는 그대로인 것처럼, Spring AI에서도 OpenAI → Azure OpenAI → Mistral로 모델을 바꿔도 서비스 코드는 거의 변경 없이 유지된다.

💡 한 줄 요약

"AI 기능도 결국 HTTP 요청을 받고, 서비스 계층에서 처리하고, DTO로 응답하는 일반적인 Spring Boot 구조 안에 들어간다."

질문 Spring AI에서의 답
LLM 호출은 어디에 두는가? 별도 AI Service 계층
프롬프트는 어떻게 관리하는가? PromptTemplate + 정책으로 운영
응답을 서비스 로직에 어떻게 연결하는가? DTO 기반 Structured Output
사내 문서 기반 답변은? RAG + VectorStore
DB / API 조회를 AI에게 시키려면? Tool Calling
Agent는 언제 필요한가? RAG + Tool을 선택적으로 조합해야 할 때
02 — Core Components
핵심 구성요소 정리

Spring AI의 구성요소들은 각각 독립적이지만 실제 서비스에서는 하나의 요청 처리 흐름 안에서 순서대로 연결된다. 목록을 암기하는 것보다 "이게 요청 흐름에서 어느 위치에 있는가"를 기준으로 이해하는 게 훨씬 빠르다.

🔌
ChatClient
LLM 호출을 위한 fluent API. WebClient처럼 체이닝 방식으로 사용. 서비스 코드의 주 진입점.
⚙️
ChatModel
모델 호출의 저수준 추상화. ChatClient의 내부 엔진. 직접 쓰기보다 ChatClient를 통해 사용.
📝
PromptTemplate
변수가 있는 프롬프트 템플릿. 역할·조건·출력 형식을 분리해서 관리.
📦
Structured Output
AI 텍스트 응답을 Java 객체(Record/DTO)로 자동 변환. .entity(Class) 호출.
🔢
EmbeddingModel
텍스트를 벡터(숫자 배열)로 변환. RAG의 기반 기술.
🗄️
VectorStore
벡터를 저장하고 유사도 기반 검색. pgvector, Pinecone 등 다양한 구현체 지원.
🔗
Advisor
ChatClient 호출 전후에 끼어드는 인터셉터. RAG, 로깅, 메모리 등을 여기 붙임.
🛠️
Tool Calling
모델이 직접 못 하는 일(DB 조회, API 호출)을 애플리케이션 함수로 위임.
03 — Architecture
백엔드 계층 어디에 AI를 두는가

AI 호출을 Controller에 직접 넣거나 기존 Service에 합쳐버리면 운영 단계에서 문제가 생긴다. 프롬프트 정책, 응답 형식, 실패 처리, 비용, 로그, 도구 권한이 전부 얽히기 때문이다. 그래서 별도 AI Service 계층으로 분리하는 게 낫다.

Controller
HTTP 요청/응답
Application Service
비즈니스 정책 검증
AI Service
ChatClient + Advisor + Tool
External
LLM / VectorDB / API
패키지 구조 예시
src/main/java/com/example/springai
├── chat
│   ├── ChatController.java
│   ├── ChatService.java
│   └── dto/
├── prompt
│   └── PromptCatalog.java
├── structured
│   ├── InquiryAnalysisResponse.java
│   └── InquiryAnalysisService.java
├── rag
│   ├── DocumentIngestionService.java
│   ├── RagChatService.java
│   └── RagController.java
├── tool
│   ├── OrderTools.java
│   └── ToolChatService.java
└── agent
    └── AgentRouterService.java
04 — ChatClient
ChatClient 기본 사용법

공식 문서에서는 ChatClient를 "AI 모델과 통신하기 위한 fluent API"로 설명한다. 동기 호출과 스트리밍 모두 지원한다. 처음 다루는 예제는 복잡할 필요가 없다 — user()에 메시지 넣고, call()로 호출하고, content()로 응답 꺼내는 것이 전부다.

ChatService.java — 가장 단순한 형태
@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String ask(String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

실무에서는 ChatClient를 매번 새로 만들기보다, 공통 시스템 프롬프트나 Advisor를 붙인 Bean으로 등록해두는 경우가 많다. 모든 답변에 언어 제한, 응답 스타일, 로깅을 일괄 적용할 수 있어서 편하다.

AiClientConfig.java — Bean으로 공통 설정
@Configuration
public class AiClientConfig {

    @Bean
    ChatClient serviceChatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("""
                        당신은 Spring Boot 백엔드 개발자를 돕는 AI 어시스턴트입니다.
                        답변은 한국어로 작성하고, 모르는 내용은 추측하지 않습니다.
                        코드 예시는 설명 가능한 최소 단위로 제공합니다.
                        """)
                .build();
    }
}
🔑 실무 포인트

ChatModel은 모델 호출의 엔진, ChatClient는 서비스 코드에서 쓰는 손잡이다. 인터페이스 기반이라 OpenAI → Azure OpenAI → Mistral 교체 시 서비스 코드 변경이 거의 없다.

05 — Prompt Engineering
프롬프트 엔지니어링 레이어

프롬프트 엔지니어링은 좋은 문장을 쓰는 기술이 아니다. 백엔드 관점에서 보면 프롬프트를 운영 가능한 입력 계약으로 만드는 일이다. 역할, 업무 맥락, 입력 변수, 출력 형식, 금지 조건을 분리해서 관리해야 품질이 일정하게 유지된다.

나쁜 프롬프트 좋은 프롬프트
"이 질문에 답해줘: {question}" 역할 정의 + 답변 기준 + 출력 형식 + 모를 때 행동 포함
매번 다른 응답 품질 PromptCatalog로 중앙 관리, 변수만 교체
PromptCatalog.java — 템플릿 중앙 관리
public class PromptCatalog {

    public static String customerSupportPrompt(String product, String question) {
        var template = new PromptTemplate("""
                당신은 {product} 서비스의 고객지원 담당자입니다.
                사용자의 질문을 읽고, 다음 기준으로 답변하세요.

                기준:
                - 확인되지 않은 정책은 단정하지 않는다.
                - 사용자가 바로 할 수 있는 다음 행동을 제안한다.
                - 답변은 5문장 이내로 작성한다.

                사용자 질문:
                {question}
                """);

        return template.render(Map.of(
                "product", product,
                "question", question
        ));
    }
}
06 — Structured Output
Structured Output — AI 응답을 DTO로 받기

자연어 응답은 화면에 보여주기엔 좋지만 시스템이 처리하기 어렵다. 실무에서는 AI 응답을 DB에 저장하거나, 위험도 기준으로 알림 발송하거나, 다음 로직을 분기해야 하는 경우가 생긴다. 그때 필요한 게 Structured Output이다. .entity(InquiryAnalysisResponse.class) 한 줄로 AI 텍스트를 Java 객체로 변환한다.

InquiryAnalysisResponse.java — 응답 DTO
public record InquiryAnalysisResponse(
        String intent,    // BILLING | BUG | ACCOUNT | GENERAL
        String summary,
        String riskLevel, // LOW | MEDIUM | HIGH
        String nextAction
) {}
InquiryAnalysisService.java
public InquiryAnalysisResponse analyze(String message) {
    return chatClient.prompt()
            .system("""
                    고객 문의를 분석하는 분류기입니다.
                    intent는 BILLING, BUG, ACCOUNT, GENERAL 중 하나로 작성하세요.
                    riskLevel은 LOW, MEDIUM, HIGH 중 하나로 작성하세요.
                    nextAction은 담당자가 바로 실행할 수 있는 행동으로 작성하세요.
                    """)
            .user(message)
            .call()
            .entity(InquiryAnalysisResponse.class); // 핵심!
}
⚠️ 주의사항

Structured Output은 모델이 JSON을 반드시 정확하게 출력한다는 보장이 없다. 운영 코드에서는 파싱 실패 시 재시도 로직이나 폴백 처리가 필요하다. Spring AI 1.1.1부터 구조화 출력의 신뢰성이 많이 개선됐다.


07 — RAG Concept
RAG 왜 필요한가, 언제 필요한가

LLM 단독 답변은 모델이 이미 알고 있는 일반 지식에 의존한다. 이 방식은 사내 정책, 최신 공지, 프로젝트 문서, 고객별 데이터처럼 모델이 학습하지 않은 정보에는 약하다. 답변이 그럴듯해 보여도 실제 문서와 다를 수 있다는 게 가장 큰 문제다.

RAG(Retrieval Augmented Generation, 검색 증강 생성)는 모델을 다시 학습시키는 것이 아니라, 질문 시점에 관련 문서를 검색하고 그 결과를 컨텍스트로 넣어 답변을 생성하는 구조다.

아래 질문들은 RAG 없이 답하면 위험하다.

질문 예시 LLM 단독 답변 위험도 RAG 필요 여부
우리 회사 환불 정책 7일 이후 처리는? 🔴 높음 — 정책이 달라도 그럴듯하게 답함 ✅ 필요
지난주 업데이트된 API 인증 방식? 🔴 높음 — 학습 데이터에 없음 ✅ 필요
Python requests 라이브러리 사용법? 🟢 낮음 — 공개 지식 ❌ 불필요
사내 온보딩 문서 기준 신규 개발자 할 일? 🔴 높음 — 회사마다 다름 ✅ 필요
08 — Naive RAG
Naive RAG 구현 흐름

Naive RAG는 "문서를 벡터로 저장하는 과정"과 "질문할 때 관련 문서를 검색하는 과정"으로 나뉜다. 많은 분들이 RAG를 한 번의 API 호출로 오해하는데, 문서 적재 시퀀스와 질문 시퀀스는 완전히 분리되어 있다.

문서 적재 시퀀스

문서 읽기
TextReader / PDFReader
청크 분할
TokenTextSplitter
임베딩 생성
EmbeddingModel
VectorStore 저장
pgvector 등

질문 시퀀스

사용자 질문
질문 임베딩
유사도 검색
프롬프트 주입
답변 생성
DocumentIngestionService.java — 문서 적재
@Service
public class DocumentIngestionService {

    private final VectorStore vectorStore;

    public void ingest(Resource resource) {
        List<Document> documents = new TextReader(resource).get();
        List<Document> chunks = new TokenTextSplitter().apply(documents);
        vectorStore.add(chunks); // 벡터 변환 + 저장 한 번에
    }
}
application.yml — 의존성 설정 예시
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4.1-mini
      embedding:
        options:
          model: text-embedding-3-small
    vectorstore:
      pgvector:
        initialize-schema: true
09 — Advisor
QuestionAnswerAdvisor 동작 원리

QuestionAnswerAdvisorChatClient에 붙이면, ChatClient 호출 전에 자동으로 VectorStore에서 관련 문서를 검색해 프롬프트 컨텍스트에 주입한다. 개발자는 검색 로직을 별도로 작성할 필요 없이 Advisor 등록 한 줄로 Naive RAG를 완성할 수 있다.

RagChatService.java
@Service
public class RagChatService {

    private final ChatClient chatClient;

    public RagChatService(ChatClient.Builder builder, VectorStore vectorStore) {
        this.chatClient = builder
                .defaultSystem("""
                        제공된 문서 컨텍스트를 우선하여 답변하세요.
                        문서에서 근거를 찾을 수 없으면 모른다고 답하세요.
                        답변 마지막에는 참고한 근거를 짧게 요약하세요.
                        """)
                .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) // 여기!
                .build();
    }

    public String askWithDocuments(String question) {
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}
📌 포인트 — RAG는 Controller가 아닌 ChatClient 구성에 붙어 있다

Controller를 보면 일반 ChatService와 코드가 거의 같다. RAG가 적용된 티가 안 나는 게 맞다. Advisor는 요청이 모델로 나가기 전, ChatClient 내부에서 조용히 동작한다.

10 — Limitations
Naive RAG 한계 & 운영 체크리스트

Naive RAG는 반드시 배워야 하는 기본형이지만, 운영에서는 이것만으로 부족한 경우가 많다. 질문이 애매하면 검색 쿼리가 약해지고, 청크 크기를 잘못 잡으면 핵심 문장이 묻히거나 문맥이 끊긴다.

  • 문서 품질 검색할 문서 자체가 최신인가? 버전 관리가 되어 있는가?
  • 청크 전략 한 청크에 하나의 의미 단위가 들어가는가? 토큰 크기는 적절한가?
  • 메타데이터 활용 부서, 버전, 날짜, 문서 타입을 메타데이터로 저장했는가? 필터 검색 가능한가?
  • 검색 쿼리 품질 사용자 질문을 그대로 검색해도 충분한가? Query Rewrite 필요하지 않은가?
  • 응답 검증 답변이 실제로 검색 문서에 근거하고 있는가? Hallucination 확인 방법은?
  • similarityThreshold 기본값 0.0(모든 결과 허용) 그대로 두면 관련 없는 문서가 붙는다. 0.7 전후로 조정 필요.
메타데이터 포함 문서 적재 예시
Document chunk = new Document(
    "환불은 결제일 기준 7일 이내 요청할 수 있습니다.",
    Map.of(
        "source",  "refund-policy.md",
        "version", "2026-06",
        "domain",  "billing"
    )
);

11 — Real-World Usage
현업에서 어떻게 활용하는가

국내외 사례를 보면 Spring AI는 주로 세 가지 방향으로 쓰인다. 기존 Java 서비스에 AI를 붙이는 경우, 보안 이슈로 외부 파이프라인을 못 쓰는 환경, 그리고 Python AI 서버 없이 Spring Boot 하나로 끝내야 하는 빠른 PoC 상황이다.

1
고객 문의 자동 분류 + 위험도 태깅
Structured Output을 이용해 문의 내용을 BILLING / BUG / ACCOUNT로 분류하고, riskLevel을 함께 뽑아낸다. HIGH 건은 즉시 담당팀 Slack 알림, LOW 건은 자동 답변 큐로 분기. 기존 CS 티켓 시스템 옆에 붙는 구조라 인프라 변경 없이 도입 가능하다.
Structured OutputPromptTemplate
2
사내 문서 Q&A 시스템 (내부 챗봇)
온보딩 가이드, 사내 정책, API 스펙 문서를 pgvector에 적재해두고 QuestionAnswerAdvisor로 연결. "결제 실패 시 환불 정책이 뭐야?" 같은 질문에 실제 문서 기반 답변을 제공한다. 민감 데이터가 외부로 나가지 않아야 하는 금융·공공 환경에서 특히 유효하다.
RAGVectorStoreQuestionAnswerAdvisor
3
Tool Calling으로 DB 조회 위임
"내 최근 주문 상태 알려줘"에서 AI가 직접 주문 DB를 조회하도록 Tool을 등록한다. 사용자 입력에서 주문 ID를 파싱하고, Tool을 통해 OrderService를 호출하고, 결과를 자연어로 변환해 응답하는 흐름. 기존 서비스 로직을 그대로 재활용할 수 있다는 게 장점이다.
Tool CallingAgent
4
Observability — AI 호출도 모니터링 대상이다
Spring AI는 Micrometer 기반 Observability를 내장한다. ChatClient, EmbeddingModel, VectorStore 호출에서 메트릭과 트레이스가 자동 수집된다. Prometheus + Grafana로 LLM 호출 비용, 지연, 에러율을 추적할 수 있다. SLO 설정 전에 먼저 측정부터 해두는 게 좋다.
ObservabilityMicrometer
🏦 금융·공공 환경 특이점

보안이 중요한 환경에서 별도 AI 서버를 두는 방식보다 Spring AI를 쓰면 민감 데이터가 외부 파이프라인으로 나가지 않고, Spring Security 인증·로깅·감사 정책을 그대로 적용할 수 있다는 장점이 있다.

12 — References
참고 아티클
📄
Spring AI 공식 문서 — Advisors API
QuestionAnswerAdvisor, RetrievalAugmentationAdvisor 등 Advisor 종류와 동작 원리를 공식 문서에서 확인할 수 있다. Modular RAG 아키텍처 설명도 있음.
docs.spring.io/spring-ai/reference/api/advisors.html
📝
Using RAG and Vector Store with Spring AI — Piotr's TechBlog
RetrievalAugmentationAdvisor와 VectorStoreDocumentRetriever를 조합한 고급 RAG 구성 예제. similarityThreshold 설정 실무 팁 포함.
piotrminkowski.com/2025/02/24/using-rag-and-vector-store-with-spring-ai/
📝
Java Spring Boot & Generative AI Guide 2025 — BrillianTech
엔터프라이즈 Java 팀을 위한 GenAI 아키텍처 패턴 가이드. Workflow vs Agent 차이, RAG, Observability(SLO, 비용 추적) 섹션이 특히 실용적.
brilliantechsoft.com/blog/java-spring-boot-meets-generative-ai-practical-guide
📝
Spring AI, 별론 줄 알았는데… 왜 이제 알았을까? — 이랜서 블로그
Spring AI를 써야 하는 상황과 쓰지 말아야 하는 상황(AI 자체가 서비스 핵심이면 Python 생태계가 낫다)을 명확히 구분해서 설명. 도입 판단 기준으로 유용.
elancer.co.kr/blog/detail/926
📚
이것이 Spring AI다 (개정판) — 한빛미디어
RAG, Tool Calling, MCP Server, 멀티 에이전트까지 다루는 국내 Spring AI 책. 실행 가능한 예제 코드 중심 구성. 특강 내용을 더 깊이 파고들 때 적합.
hanbit.co.kr/store/books/look.php?p_code=B8856838821
🗂 핵심 요약 & 반드시 알아야 할 것들
구조 원칙
AI 호출은 별도 AI Service 계층으로 분리. Controller에 직접 넣지 않는다. 프롬프트·응답 형식·실패 처리가 전부 여기 몰린다.
ChatClient 핵심
공통 Bean으로 등록해서 defaultSystem(), defaultAdvisors() 설정. 모델 교체 시 서비스 코드 변경 없이 설정만 바꾼다.
Structured Output
.entity(MyRecord.class) 한 줄로 AI 응답을 DTO로 변환. DB 저장·분기 처리·알림 로직 연결에 필수.
RAG 두 개의 시퀀스
적재(문서→청크→임베딩→저장)와 질문(검색→주입→생성)은 완전히 분리. 적재는 배치, 질문은 실시간.
Advisor 위치
RAG는 Controller나 Service가 아니라 ChatClient 구성에 붙는다. defaultAdvisors()에 등록하면 모든 호출에 자동 적용.
Naive RAG 체크리스트
문서 최신성, 청크 전략, 메타데이터, similarityThreshold 설정 (0.0 그대로 두면 관련 없는 결과 포함). 운영 전 반드시 점검.

Bonus — Apply to My Projects
내 프로젝트에 어디 적용하면 좋을까

Spring AI는 기능이 많아서 "어디부터 붙이지?"가 막막할 수 있다. 진입 비용이 낮으면서 효과가 명확한 곳부터 붙이는 게 현실적이다.

A
택배 조회 서비스 (whereIsMyParcel, DelivHub)
배송 상태 변환(코드 → 자연어 설명)에 Structured Output을 붙이거나, "내 택배 왜 늦어요?" 같은 사용자 문의를 의도 분류 + 자동 응답에 연결할 수 있다. 이미 조회 API가 있으니 Tool Calling으로 AI가 직접 조회하도록 연결하는 것도 구조상 깔끔
Structured OutputTool Calling
B
금융 문해력 플랫폼 (ssac.io)
"페르소나 기반 콘텐츠 추천" 흐름에 RAG + VectorStore가 직접 맞닿아 있다. 금융 용어 설명 문서, 학습 콘텐츠를 벡터로 적재해두고, 사용자의 학습 수준과 관심사에 맞는 문서를 검색해서 답변을 구성하는 구조가 자연스럽다. 온보딩 시 사용자 페르소나를 파악하는 단계에서 Structured Output으로 페르소나 분류 DTO를 뽑아내는 것도 실용적인 시작점
RAGStructured OutputPromptTemplate
💬 적용 순서 제안

1단계: Structured Output으로 기존 문자열 응답을 DTO화 → 제일 쉽고 임팩트 있음
2단계: PromptCatalog 만들어서 프롬프트 중앙 관리 시작
3단계: 사내/서비스 문서 기반 Q&A가 필요해지면 Naive RAG 도입
4단계: 모델이 직접 DB·API를 다뤄야 하면 Tool Calling 추가

'스파르타 심화 과정' 카테고리의 다른 글

모니터링과 부하테스트  (0) 2026.06.02
CQRS  (0) 2026.05.26
spring 과제  (0) 2026.04.21
4/20 Spring 기초(2) 특강  (0) 2026.04.20
spring 특강 1  (0) 2026.04.20