JSCODE Logo
JSCODE 박재성JSCODE 제이온JSCODE 시니
온라인 강의프로그래밍 과외책/전자책무료 강의
유튜브블로그
후기
완강 후기 이벤트블로그 리뷰 이벤트AWS SAA 합격 후기 이벤트
회사명 : JSCODE대표 : 박재성사업자 등록번호 : 244-22-01557통신판매업 : 제 2023-인천미추홀-0381 호

서울특별시 구로구 경인로 20가길 11(오류동, 아델리아)

Copyright ⓒ 2026 JSCODE - 최상위 현업 개발자들의 프로그래밍 교육 All rights reserved.

이용약관개인정보처리방침

실무에 바로 적용하는 Spring AI: Spring 서비스에 챗봇·RAG·MCP 도입하기

백엔드 개발자를 위한 LLM 기초

Prompt란 무엇일까?
Tokens란 무엇일까? /
Embeddings란 무엇일까? / Semantic Space
Fine Tuning이란 무엇일까?
RAG란 무엇일까?
AI Models란 무엇일까?
AI Model Providers란 무엇일까?

Spring AI 기본기

Spring AI란 무엇일까? / 백엔드 개발자가 왜 Spring AI를 배워야할까?
Spring AI 1.0 GA의 탄생
Spring AI와 LangChain - 어떤 도구를 선택해야 할까?
Spring AI와 LangChain - Chat Model 쉽게 이해하기
Spring AI와 LangChain - VectorStore 인터페이스

Spring AI 핵심 구성요소

핵심 컴포넌트 - ChatClient, Advisors, Prompts, Models, Structured Output
Spring AI ChatClient란 무엇일까?
Spring AI Advisor란 무엇일까?
Spring AI Prompts란 무엇일까?
Spring AI Models이란 무엇일까?
Spring AI Structured Output이란 무엇일까?

Spring AI로 만드는 채팅 API — 클라우드(OpenAI)와 로컬(Ollama

[실습] Spring AI 로컬 환경 설정 - Spring Boot 4.x, Gradle
[실습] OpenAI API Key 발급받기
[실습] OpenAI API를 활용한 채팅 API 환경 구축
[실습] Swagger UI를 활용한 테스트 - OpenAI API
[실습] Ollama 설치하기 / Hugging Face란 무엇일까? / GGUF
[실습] Ollama를 활용한 채팅 API 환경 구축
[실습] Swagger UI를 활용한 테스트 - Ollama
Spring AI 응답의 형태 - ChatClient Responses
[실습] Streaming으로 Flux 형태로 응답받기

실무에 쓰는 챗봇 만들기 - ‘대화 기억’부터 ‘CS 문의 자동 분류’까지

Spring AI Prompts - Chat Options란 무엇일까?
Spring AI Prompts - 프롬프트 엔지니어링 9가지 패턴
[실습] CLI 챗봇 개발 환경 아키텍처 미리보기
[실습] Chat Memory Advisor로 메모리 기능 추가하기
[실습] Chat Service 구현 - Advisor, CONVERSATION_ID
[실습] Spring Boot에서 CLI 기능 구현 및 테스트하기
[실습] Chat Controller 구현 - call과 stream 방식의 Chat REST API
[실습] Structured Output Converter를 통한 쇼핑몰 CS 문의 긴급도 분류하기

Vector Database와 유사도 검색 - RAG를 위한 핵심 개념

Vector Database란 무엇일까?
Vector Database Cosine Similarity란 무엇일까? / Spring AI의 Similarity Score
Spring AI Vector Store API
Metadata Filters란 무엇일까?

RAG 챗봇 만들기 - 데이터 전처리부터 ElasticSearch 연동까지

LLM 한계 극복을 위한 RAG 도입
실무에서 Spring AI를 통한 RAG 구현 방법
고품질 RAG를 위한 데이터 전처리 - Extract, Transform, Load
Spring AI RAG 기본 설계
[실습] RAG 챗봇 개발을 위한 로컬 환경 구축
[실습] RAG ETL 파이프라인 구현
[실습] RAG Advisors 구현
[실습] RAG Service 구현
[실습] RAG CLI 구현 및 실행 테스트
[실습] RAG REST API 구현
[실습] Docker 설치
[실습] ElasticSearch 기반 VectorStore로 변경하기

AI와 외부 시스템 연결하기 - Tool Calling으로 API 호출 자동화

Spring AI Tool Calling과 작동 프로세스
[실습] Tool Calling 개발 환경 구축
[실습] 지역별 날씨 조회 외부 API Tool 구현
[실습] Tool Service 구현
[실습] ToolCallingManager 설정
[실습] ToolChatConfig, ToolController 구현 - CLI, REST API
[실습] CLI 및 REST API 테스트

MCP로 완성하는 AI 연동 - Client & Server 개발과 외부 서버 활용

MCP란 무엇일까?
Tool Calling VS MCP
[실습] 아키텍처 미리보기
[실습] MCP Client 개발 환경 구축
[실습] MCP Server 개발 환경 구축
[실습] 외부 MCP Server와 연동한 실시간 날씨 조회 - MCP Client 개발
[실습] 외부 MCP Server와 연동한 실시간 날씨 조회 - MCP Server 개발
[실습] 외부 MCP Server와 연동한 실시간 날씨 조회 - MCP 연동
[실습] CLI 및 RestAPI 테스트
← 블로그 목록으로 돌아가기

[실습] RAG ETL 파이프라인 구현

JSCODE 시니
JSCODE 시니
2026. 06. 13.
author
JSCODE 시니
category
Spring AI
createdAt
Jun 13, 2026 12:53 AM
isPublic
isPublic
series
실무에 바로 적용하는 Spring AI: Spring 서비스에 챗봇·RAG·MCP 도입하기
slug
practice-implementing-rag-etl-pipeline
type
post
updatedAt

✅ 1. Extract 구현

application.yaml
app: rag: documents-location-pattern: classpath:spring-ai-llm.pdf
RagConfig.java
package com.jscode.chat.rag; @Configuration public class RagConfig { @Bean public List<DocumentReader> documentReaders(@Value("${app.rag.documents-location-pattern}") String documentsLocationPattern) throws IOException { // 1. 설정한 경로 패턴에 맞는 파일 찾기 PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource[] resources = resolver.getResources(documentsLocationPattern); // 2. 찾아온 파일들을 담을 빈 리스트 생성 List<DocumentReader> readers = new ArrayList<>(); // 3. 파일의 개수만큼 for문을 돌면서 tika로 예쁘게 포장해서 리스트에 넣기 for(Resource resource : resources){ readers.add(new TikaDocumentReader(resource)); } // 4. 리스트 반환 return readers; } }
 
 
 

✅ 2. Transform 구현

  • 영어 ‘Apple’은 1토큰이지만, 한글 ‘사과’는 모델에 따라 2~3토큰으로 쪼개 지기도함
  • 이것을 방지하기 위해 글자수 그대로 자르는 로직을 구현해보자
notion image
LengthTextSplitter.java
package com.jscode.chat.rag; import org.springframework.ai.transformer.splitter.TextSplitter; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.List; /* 글자수 기반 슬라이딩 윈도우 텍스트 분할기 긴 문서를 지정된 글자수(chunkSize)만큼 자르되, 문맥이 끊기지 않도록 이전 조각과 일정 부분을 겹치게(chunkOverlap) 자르는 클래스 */ public class LengthTextSplitter extends TextSplitter { private final int chunkSize; // 한 조각(Chunk)의 최대 글자 수 (예: 1000자) private final int chunkOverlap; // 다음 조각과 겹치게 할 글자 수 (예: 200자) /* 객체를 생성할 때 자를 크기와, 겹칠 크기를 필수적으로 받기! */ public LengthTextSplitter(int chunkSize, int chunkOverlap) { this.chunkSize = chunkSize; this.chunkOverlap = chunkOverlap; } @Override protected List<String> splitText(String text) { // "안녕하세요반갑습니다"(10글자), chunk:5, over:2 // 1. 잘라낼 조각들을 담을 빈 리스트 생성 List<String> chunks = new ArrayList<>(); // 2. 만약 넘어온 텍스트가 비어있거나, 공백뿐이라면 자를게 없으므로 그대로 리턴 if(!StringUtils.hasText(text)){ return chunks; } int textLength = text.length(); // 10글자 // 3. 자르기 시작할 '시작점(인덱스)'를 0으로 세팅 int chunkStart = 0; // 4. 끝까지 반복해서 자르기 while (chunkStart < textLength) { // [끝점 계산] 자를 조각의 끝점 계산 // (시작점 + 자를 크기)를 하되, 만약 남은 글자가 부족해서 전체 길이를 넘어가 버리면 전체 길이에서 딱 멈추기 int chunkEnd = Math.min(chunkStart + chunkSize, textLength); // 5,10중에 작은값 5 / 8,10 중에 작은값 8 // [자르기] String slicedText = text.substring(chunkStart, chunkEnd); // "안녕하세요", "세요반갑습" chunks.add(slicedText); // [다음 시작점 계산] // 방금 자른 조각의 '끝점'에서 '겹칠 크기(Overlap)'만큼 뒤로 되돌아간 곳이 다음 번 시작점이 됨 // 이렇게 해야 다음 조각을 자를 때 이전 조각의 뒷부분이 자연스럽게 포함됨. int nextStart = chunkEnd - chunkOverlap; // 3 // 남은 텍스트가 너무 짧거나 설정 오류로 인해 다음 시작점이 제자리에 머물거나 뒤로 밀리면 무한루프에 빠짐. // 이를 방지하기 위해 강제로 반복문을 탈출 if (nextStart <= chunkStart) { break; } // [위치 이동] 시작점을 방금 계산한 다음 시작점으로 이동시키고 다시 루프를 돌기 chunkStart = nextStart; // 3 } // 5. 리스트 반환 return chunks; } }
RagConfig.java
@Bean public DocumentTransformer textSplitter(){ return new LengthTextSplitter(200,100); } @Bean public DocumentTransformer keywordMetadataEnricher(ChatModel chatModel){ return new KeywordMetadataEnricher(chatModel, 4); }
 
 
 

✅ 3-1. Load 구현

  • 원본 문서가 텍스트 분할기에 의해 어떻게 쪼개졌는지 확인해보기
  • VectorStore에 담기는 데이터를 저장하기 전 확인!
RagConfig.java
@Bean public DocumentWriter jsonConsoleDocumentWriter(ObjectMapper objectMapper){ // 앞 단계에서 가공되어 넘어온 문서 조각 리스트(documents)를 받아서 로직실행 return documents -> { // 1. 현재 들어온 총 문서 조각(Chunk)의 개수가 몇 개인지 콘솔에 명확하게 표기 System.out.println("======= 저장할 문서 조각(Chunk) 개수: " + documents.size() + " ========"); try { // 들여쓰기와 줄바꿈 적용된 예쁜 JSON 문자열로 출력 String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(documents); System.out.println(jsonString); } catch (Exception e) { System.out.println("JSON 변환 중 에러가 발생했습니다: " + e.getMessage()); } System.out.println("======================================================"); }; }
 
 
 

✅ 3-2 Load 구현

build.gradle
implementation 'org.springframework.ai:spring-ai-starter-model-ollama' // implementation 'org.springframework.ai:spring-ai-starter-model-openai'
  • 임베딩 모델은 ollama의 모델로 사용하기 위해 해당 부분 변경
#[2] Ollama 설정 ollama: init: pull-model-strategy: when_missing chat: model: hf.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF embedding: options: model: bge-m3
 
 
 

✅ 4. ETL 파이프라인 구성하기

application.yaml
app: rag: documents-location-pattern: classpath:spring-ai-llm.pdf etl: pipeline: init: true
RagConfig.java
@ConditionalOnProperty(prefix = "app.etl.pipeline", name = "init", havingValue = "true") @Order(1) // 다른 실행 코드들보다 가장 먼저 파이프라인을 가동하라 @Bean public ApplicationRunner initEtlPipeline( List<DocumentReader> documentReaders, // 1. Extract DocumentTransformer textSplitter, // 2. Transform DocumentTransformer keywordMetadataEnricher, List<DocumentWriter> documentWriters) { // 3. Load(콘솔 출력기, VectorDB 등) return args -> { System.out.println("[System] ETL 파이프라인 가동 시작"); // 1. 등록된 모든 파일 리더기(Reader)들을 하나씩 꺼내서 실행 for (DocumentReader reader : documentReaders) { // 1. Extract 원본 파일에서 거대한 텍스트 덩어리를 읽어오기 List<Document> rawDocuments = reader.get(); System.out.println("[Extract] 파일 읽기 완료"); // 2. Transform 읽어온 문서를 AI가 소화하기 좋게 조각조각(Chunk) 자르기 List<Document> chunkedDocuments = textSplitter.apply(rawDocuments); System.out.println("[Transform] 문서 분할 완료"); // 키워드 추출기 이어서 적용 chunkedDocuments = keywordMetadataEnricher.apply(chunkedDocuments); // 3. Load 가공된 문서 조각들을 준비된 모든 저장소에 집어넣음 for (DocumentWriter writer : documentWriters) { writer.accept(chunkedDocuments); } System.out.println("[Load] 저장소 적재 완료"); } System.out.println("[System] ETL 파이프라인 적재 종료"); }; }