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 테스트
← 블로그 목록으로 돌아가기

[실습] 외부 MCP Server와 연동한 실시간 날씨 조회 - MCP Server 개발

JSCODE 시니
JSCODE 시니
2026. 06. 13.
author
JSCODE 시니
category
Spring AI
createdAt
Jun 13, 2026 09:55 AM
isPublic
isPublic
series
실무에 바로 적용하는 Spring AI: Spring 서비스에 챗봇·RAG·MCP 도입하기
slug
practice-mcp-server-development-for-weather-service
type
post
updatedAt

✅ 1. application.yaml 추가

application.yaml
app: chat: default-system-prompt: 한국어를 사용하는 tool 지원 AI 입니다. # 기본 system prompt etl: pipeline: init: true # 초기 데이터 로딩 여부 vectorstore: in-memory.enabled: true # in-memory SimpleVectorStore 사용 여부 rag: documents-location-pattern: classpath:spring-ai-llm2.pdf # RAG Data 위치 logging: level: io: modelcontextprotocol: DEBUG org: springframework: web: DEBUG ai: chat: client: advisor: DEBUG # SimpleLoggerAdvisor 등의 Advisor에서 DEBUG 로그 출력 tool: DEBUG # Tool 사용 관련 DEBUG 로그 출력 mcp: DEBUG # mcp 사용 관련 DEBUG 로그 출력 file: path: log name: log/${spring.application.name}.log server: port: 8081 spring: application: name: mcp-server ai: mcp: server: name: ${spring.application.name} version: 0.1.0 type: SYNC instructions: "Spring AI 개념을 RAG 를 사용해 제공하는 MCP 서버" sse-message-endpoint: /mcp/messages model: chat: ollama # 여러 Chat 모델 사용시 auto-configurations 에서 사용할 모델 설정 필요 예: openai, ollama embedding: ollama # 여러 Embedding 모델 사용시 auto-configurations에서 사용할 모델 지정 (예: openai, ollama) ollama: init: pull-model-strategy: when_missing # when_missing, always, never 설정 chat: options: model: hf.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF embedding: options: model: bge-m3
💁‍♀️
강의 영상에서 instructions이 잘못 표기되어, 강의자료에 반영해두었습니다! 해당 부분 참고 부탁드립니다.
 
 
 

✅ 2. logback-spring.xml 설정

logback-spring.xml
<configuration> <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender"> <target>System.err</target> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="STDERR" /> </root> </configuration>
  • MCP 통신 방식 중 stdio(표준입출력)을 사용할 때는 서버에서 절대로 stdout(표준출력)을 사용하면 안됨
  • 따라서 해당 로그 설정을 반드시 해주어야함
 
 
 

✅ 3. MCP Config

McpServerConfig.java
package com.jscode.mcpserver; @Configuration public class McpServerConfig { @Bean public SimpleLoggerAdvisor simpleLoggerAdvisor() { return SimpleLoggerAdvisor.builder().build(); } // MCP 클라이언트가 접속해서 도구를 사용하려면 반드시 필요함(RAG) @Bean public ToolCallbackProvider toolCallbackProvider(Tools tools) { return MethodToolCallbackProvider.builder().toolObjects(tools).build(); } }
 
 
 

✅ 4-1. Rag 관련 설정

  • LengthTextSplitter
  • RagChatService
  • RagConfig
  • MCP 클라이언트(Claude 데스크톱 앱 등)와 스프링 서버가 stdio(표준 입출력) 방식으로 연결된 경우, 둘은 오직 System.out을 통해 엄격한 JSON-RPC 포맷의 데이터만 주고받기로 약속되어 있음
  • 따라서 System.out.println("======= 저장할 문서 조각(Chunk) 개수... ========"); 같은 단순 텍스트가 콘솔에 찍히면 파싱 에러가 나서 강제종료될 수 있음
  • 이 부분은 로거로 수정할 것
 
 
 

✅ 4-2. 의존성 추가

compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
 
 
 

✅ 4-3. log 변경

package com.jscode.mcpserver.rag; @Slf4j @Configuration public class RagConfig { /* Extract */ @Bean public List<DocumentReader> documentReaders(@Value("${app.rag.documents-location-pattern}") String documentsLocationPattern) throws IOException { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource[] resources = resolver.getResources(documentsLocationPattern); List<DocumentReader> readers = new ArrayList<>(); for(Resource resource : resources){ readers.add(new TikaDocumentReader(resource)); } return readers; } /* Transform */ @Bean public DocumentTransformer textSplitter(){ return new LengthTextSplitter(200,100); } /* Load */ @Bean public DocumentWriter jsonConsoleDocumentWriter(ObjectMapper objectMapper){ return documents -> { log.info("======= 저장할 문서 조각(Chunk) 개수: {} ========", documents.size()); try { String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(documents); log.debug("Chunk JSON: {}", jsonString); } catch (Exception e) { log.error("JSON 변환 중 에러가 발생했습니다: {}", e.getMessage()); } log.info("======================================================"); }; } @ConditionalOnProperty(prefix = "app.vectorstore.in-memory", name = "enabled", havingValue = "true") @Bean public VectorStore vectorStore(EmbeddingModel embeddingModel){ return SimpleVectorStore.builder(embeddingModel).build(); } @ConditionalOnProperty(prefix = "app.etl.pipeline", name = "init", havingValue = "true") @Order(1) @Bean public ApplicationRunner initEtlPipeline( List<DocumentReader> documentReaders, DocumentTransformer textSplitter, DocumentTransformer keywordMetadataEnricher, List<DocumentWriter> documentWriters) { return args -> { log.info("[System] ETL 파이프라인 가동 시작"); for (DocumentReader reader : documentReaders) { List<Document> rawDocuments = reader.get(); log.info("[Extract] 파일 읽기 완료"); List<Document> chunkedDocuments = textSplitter.apply(rawDocuments); log.info("[Transform] 문서 분할 완료"); chunkedDocuments = keywordMetadataEnricher.apply(chunkedDocuments); for (DocumentWriter writer : documentWriters) { writer.accept(chunkedDocuments); } log.info("[Load] 저장소 적재 완료"); } log.info("[System] ETL 파이프라인 적재 종료"); }; } @Bean public RetrievalAugmentationAdvisor retrievalAugmentationAdvisor(VectorStore vectorStore, ChatClient.Builder chatClientBuilder, Optional<DocumentPostProcessor> printDocumentsPostProcessor){ VectorStoreDocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .similarityThreshold(0.3) .topK(3) .build(); ContextualQueryAugmenter queryAugmenter = ContextualQueryAugmenter.builder() .allowEmptyContext(true) .build(); MultiQueryExpander queryExpander = MultiQueryExpander.builder() .chatClientBuilder(chatClientBuilder) .build(); TranslationQueryTransformer queryTransformer = TranslationQueryTransformer.builder() .chatClientBuilder(chatClientBuilder) .targetLanguage("korean") .build(); RetrievalAugmentationAdvisor.Builder advisorBuilder = RetrievalAugmentationAdvisor.builder() .documentRetriever(documentRetriever) .queryAugmenter(queryAugmenter) //.queryExpander(queryExpander) //.queryTransformers(queryTransformer) ; printDocumentsPostProcessor.ifPresent(processor -> advisorBuilder.documentPostProcessors(processor)); return advisorBuilder.build(); } @ConditionalOnProperty(prefix = "app.cli", name = "enabled", havingValue = "true") @Bean public DocumentPostProcessor printDocumentsPostProcessor() { return (query, documents) -> { log.info("[ Search Results ]"); log.info("==============================================="); if (documents == null || documents.isEmpty()) { log.info(" No search results found."); log.info("==============================================="); return documents; } for (int i = 0; i < documents.size(); i++) { Document document = documents.get(i); // String.format을 사용하여 소수점 둘째 자리까지 잘라서 로깅 log.info("▶ {} Document, Score: {}", i + 1, String.format("%.2f", document.getScore())); log.info("-----------------------------------------------"); Optional.ofNullable(document.getText()).stream() .map(text -> text.split("\n")).flatMap(Arrays::stream) // 중괄호 바인딩을 통해 혹시 모를 텍스트 내의 특수문자 에러 방지 .forEach(line -> log.info("{}", line)); log.info("==============================================="); } log.info("[ RAG 사용 응답 ]"); return documents; }; } }
 
 
 

✅ 5. Tools 구현

package com.jscode.mcpserver.tool; @Component public class Tools { private static final Logger log = LoggerFactory.getLogger(Tools.class); private final RagChatService ragChatService; public Tools(@Lazy RagChatService ragChatService) { this.ragChatService = ragChatService; } @Tool(description = "Spring AI 개념에 대해 RAG 기반 답변을 제공합니다.", returnDirect = true) public String ragTool(@ToolParam(description = "Spring AI 강의에 대한 질문") String userPrompt) { log.info("ragTool UserPrompt = {}", userPrompt); return this.ragChatService.call(Prompt.builder().messages(UserMessage.builder().text(userPrompt).build()).build(),"mcp", Optional.empty()) .getResult().getOutput().getText(); } }
The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | ragChatService ↑ ↓ | chatClientBuilder ↑ ↓ | ollamaChatModel ↑ ↓ | toolCallingManager ↑ ↓ | toolCallbackResolver ↑ ↓ | toolCallbackProvider ↑ ↓ | tools └─────┘
  • 순환 참조로 인한 @Lazy 추가