2026. 5. 28. 19:04ㆍ대우개발원 수업 내용/spring기반 ai

1일차 작업한것
mpcclientchat 프로젝트 생성
application.yaml 코드 추가
spring:
application:
name: mpcclientchat
ai:
mcp:
client:
enabled: true # MCP 클라이언트를 활성화
name: fs-chat-client # MCP 서버에 전달되는 클라이언트 이름
version: 1.0.0
type: SYNC # 동기 방식(Synchronous)으로 MCP 서버와 통신
request-timeout: 60s # MCP 서버 응답 대기 시간
stdio:
root-change-notification: true # 워크스페이스(root directory) 변경 시 MCP 서버에 알림 전송
servers-configuration: classpath:mcp-servers.json
openai:
api-key:
chat:
options:
model: 'gpt-4o-mini'
logging:
level:
io.modelcontextprotocol: DEBUG
org.springframework.ai.mcp: DEBUG
이전 프로젝트에서 만들었던 mpc-servers.json 파일을 reoutces 폴더 안에 넣음

{
"mcpServers": {
"filesystem" :{
"command": "E:\\3\\node_js\\npx.cmd",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"E:\\daewooproject\\springaimcp"
]
}
}
}
index.html 파일을 받아서 templates 폴더에 넣음

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spring AI Chatbot</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.chat-container {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.chat-history {
height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 20px;
background-color: #fafafa;
}
.message {
margin-bottom: 15px;
padding: 10px;
border-radius: 8px;
}
.user-message {
background-color: #007bff;
color: white;
text-align: right;
margin-left: 20%;
}
.ai-message {
background-color: #e9ecef;
color: #333;
margin-right: 20%;
}
.input-group {
display: flex;
gap: 10px;
}
#messageInput {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
#sendBtn {
background-color: #007bff;
color: white;
}
#clearBtn {
background-color: #dc3545;
color: white;
}
button:hover {
opacity: 0.9;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="chat-container">
<h1>Spring AI Chatbot</h1>
<div id="chatHistory" class="chat-history">
<div th:each="chat : ${chatHistory}"
th:class="${chat.sender == 'user'} ? 'message user-message' : 'message ai-message'">
<strong th:text="${chat.sender == 'user'} ? '사용자' : 'AI'">Sender</strong>:
<span th:text="${chat.message}">Message</span>
</div>
</div>
<div class="input-group">
<input type="text" id="messageInput" placeholder="메시지를 입력하세요..." />
<button id="sendBtn" onclick="sendMessage()">전송</button>
<button id="clearBtn" onclick="clearHistory()">대화내역 지우기</button>
</div>
</div>
<script>
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (message === '') return;
// 입력 필드 비우기
input.value = '';
// 사용자 메시지 즉시 표시
addMessageToHistory('user', message);
// 버튼 비활성화
document.getElementById('sendBtn').disabled = true;
// 로딩 메시지 표시
addMessageToHistory('ai', '답변을 생성 중입니다...');
fetch('/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'message=' + encodeURIComponent(message)
})
.then(response => response.json())
.then(data => {
// 로딩 메시지 제거
removeLastMessage();
// AI 응답 추가
addMessageToHistory('ai', data.response);
// 버튼 활성화
document.getElementById('sendBtn').disabled = false;
})
.catch(error => {
console.error('Error:', error);
removeLastMessage();
addMessageToHistory('ai', '오류가 발생했습니다.');
document.getElementById('sendBtn').disabled = false;
});
}
function addMessageToHistory(sender, message) {
const chatHistory = document.getElementById('chatHistory');
const messageDiv = document.createElement('div');
messageDiv.className = sender === 'user' ? 'message user-message' : 'message ai-message';
messageDiv.innerHTML = `<strong>${sender === 'user' ? '사용자' : 'AI'}</strong>: ${message}`;
chatHistory.appendChild(messageDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
}
function removeLastMessage() {
const chatHistory = document.getElementById('chatHistory');
if (chatHistory.lastElementChild) {
chatHistory.removeChild(chatHistory.lastElementChild);
}
}
function clearHistory() {
fetch('/clear', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'cleared') {
document.getElementById('chatHistory').innerHTML = '';
}
});
}
// 엔터 키로 메시지 전송
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>
MpcclientchatApplication 코드에 추가
package com.example.mpcclientchat;
import io.modelcontextprotocol.client.McpSyncClient;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.List;
import java.util.Scanner;
@SpringBootApplication
public class MpcclientchatApplication {
public static void main(String[] args) {
SpringApplication.run(MpcclientchatApplication.class, args);
}
@Bean
public CommandLineRunner chatbot(ChatClient.Builder chatClientBuilder, List<McpSyncClient>mcpSyncClients) {
return args -> {
var chatClient = chatClientBuilder
.defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpSyncClients))
.build();
// Start the chat loop
System.out.println("\nI am your AI assistant.\n");
try (Scanner scanner = new Scanner(System.in)) {
while (true) {
System.out.println("\n USER: ");
System.out.println("\nASSISTANT: " +
chatClient.prompt(scanner.nextLine()) // Get the user input
.call()
.content());
}
}
};
}
}
실행하면


controller 패키지 만들고
ChatbotController 클래스 추가
package com.example.mpcclientchat.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Controller
public class ChatbotController {
private final List<Map<String, String>> chatHistory = new ArrayList<>();
private final ChatClient chatClient;
private SyncMcpToolCallbackProvider toolCallbackProvider;
public ChatbotController(ChatClient.Builder clientBuilder, SyncMcpToolCallbackProvider toolCallbackProvider) {
chatClient = clientBuilder.defaultToolCallbacks(toolCallbackProvider.getToolCallbacks()).build();
}
@GetMapping("/")
public String index(Model model) {
model.addAttribute("chatHistory", chatHistory);
return "index";
}
@PostMapping("/chat")
@ResponseBody
public Map<String, String> chat(@RequestParam String message) {
try {
// 사용자 메시지를 히스토리에 추가
chatHistory.add(Map.of("sender", "user", "message", message));
String response = chatClient.prompt(message)
.call()
.content();
// AI 응답을 히스토리에 추가
chatHistory.add(Map.of("sender", "user", "message", response));
return Map.of("response", response);
} catch (Exception e) {
String errorMessage = "죄송합니다. 오류가 발생했습니다: " + e.getMessage();
chatHistory.add(Map.of("sender", "user", "message", errorMessage));
return Map.of("response", errorMessage);
}
}
@PostMapping("/clear")
@ResponseBody
public Map<String, String> clearHistory() {
System.out.println("/clear 호출");
chatHistory.clear();
return Map.of("status", "cleared");
}
}
실행했을때 터미널에 USER:뒤에 입력을 하지 않으면 코드가 계속 돌아간다(while문 때문에)
따라서 @BEAN부분을 주석처리 하고 실행하면 8080이 뜨면서 클릭해서 주소로 넘어갈 수 있다

로컬에 접속하면

웹에서 url을 입력해서 접속하는 방법도 있다.



오늘은 26/5/28인데 날짜가 이상하게 나온다 json에 코드를 추가하면 된다
{
"mcpServers": {
"filesystem" :{
"command": "E:\\3\\node_js\\npx.cmd",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"E:\\daewooproject\\springaimcp"
]
},
"datetime": {
"command": "E:\\3\\node_js\\npx.cmd",
"args": [
"-y",
"@pinkpixel/datetime-mcp"
],
"env": {
"TZ": "Asia/Seoul"
}
}
}
}
실행하면

mcp로 서버를 만들기
mcpserver1 생성
application.properties 코드 추가
spring.application.name=mcpserver1
# Required STDIO Configuration
spring.ai.mcp.server.stdio=true
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
# Spring 배너 제거
spring.main.banner-mode=off
# 웹 서버 비활성화(MCP stdio 용도)
spring.main.web-application-type=none
# 콘솔 로그 제거
logging.level.root=OFF
# Server Configuration
spring.ai.mcp.server.enabled=true
spring.ai.mcp.server.name=my-dooly-server
spring.ai.mcp.server.version=0.0.1
# SYNC or ASYNC
spring.ai.mcp.server.type=SYNC
spring.ai.mcp.server.resource-change-notification=true
spring.ai.mcp.server.tool-change-notification=true
spring.ai.mcp.server.prompt-change-notification=true
# Optional file logging
logging.file.name=mcp-dooly-stdio-server.log
DoolyService 코드 추가
package com.example.mcpserver1;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
@Service
public class DoolyService {
@Tool(description = "아기공룡 둘리에 대한 정보를 제공")
public String getStory( ) {
return "아기공룡 둘리라는 만화는 1983년 4월호부터 육영재단에서 발행하던 월간 만화잡지 '보물섬'에 1983년 4월 23일부터 1993년 8월 8일까지 10년간 연재되었다. 항상 잡지의 첫 번째나 두 번째에 실렸다. 둘리가 워낙 인기 만화였기 때문에, 보물섬에서 매달 추첨하여 증정하는 선물 중엔 둘리 인형이 포함되어 있었다. 개수도 다른 선물보다는 넉넉하게 준비되어 있었다." +
"초반 연재 때는 1부, 2부, 3부로 크게 묶여 있었는데, 1부는 둘리가 고길동의 집에 와서 철수와 영희, 희동이와 만들어나가는 에피소드였고, 2부는 도우너와 또치 합류 후에 고길동의 집과 동네에서 벌이는 각종 말썽을 그렸다.[4] 그리고 3부는 둘리 일당이 희동이와 함께 나룻배를 타고 바다 한가운데로 표류하게 되면서 벌어지는 이야기로, 세계를 누비며 모험을 하는 에피소드로 구성되었다. 이런 구분은 3부가 끝나고 둘리 일당이 다시 돌아오게 되면서 흐지부지되었다." +
"각 에피소드는 옴니버스 형식이 기본이었고, 연재 1회에 에피소드 하나가 종결되는 구성을 기본으로 했다. 좀 긴 에피소드는 연재 2~3회분을 잡아먹기도 했다. 저승행차 에피소드를 연재할 때는 원래 2회분으로 잡았다가 분량 조절 실패로 3회까지 갔다. 이때 2회의 말미에 \"죄송합니다. 이야기가 넘쳐서 다음호까지 연결됩니다\"라는 작가의 사과문이 짧게 실렸다. 옴니버스물 치고는 장편 에피소드의 비중이 꽤 높은 편이며[5] 에피소드 간 연결성도[6] 강해 반쯤 스토리물이라고 봐도 될 정도의 전개를 보여주었다." +
"보물섬 연재 종료 이후 2010년 10월 25일, 17년만에 조선일보에서 '신문은 선생님'섹션에 둘리 과학여행, 둘리 호기심 나라가 연재되었다." ;
}
@Tool(description = "주인공 정보 제공")
public String getActor(String name) {
if (name.equals("둘리")) {
return "이름의 유래는 둘째에서 따왔다. 원작 만화에서 둘리 형의 이름이 '하나'라서 둘리의 이름을 둘째라는 뜻의 '두리(둘+2)'라고 지었는데 이 이름이 너무 흔해서 현재의 이름으로 바꾸게 되었다고. 참고로 '하나'는 사실 형이 아니라 누나다. 신체 나이는 8세, 종은 케라토사우루스.[19] 하지만 케라토사우루스는 수각류라 둘리의 형태와 비교적 맞는 데 비해, 원작에 나오는 \"둘리 엄마\"는 용각류라 종이 맞지 않는다. 둘리 단행본 애장판에 실린 김수정 씨의 인터뷰에 의하면 작가의 실수라고 한다. 둘리 엄마는 브론토사우루스[20] 인데, 포근한 엄마 이미지의 공룡을 찾다가 둘리가 케라토사우루스였다는 것을 잊어버리고 브론토사우루스로 그렸다고.[21] 이후 스펀지에서 이 주제를 가지고 방영했을 때는 김수정 작가 본인이 한번 더 인터뷰에 응해서 이 실수에 대해 사과하기도 했다.";
} else if (name.equals("또치")) {
return "아기공룡 둘리의 등장인물. 둘리 일당의 멤버이자 홍일점으로, 중반에 나오며, 별로 등장하지 않는 코로깨를 제외하면 가장 마지막으로 둘리 일당에 합류한 멤버다. 아프리카에서 온 암컷 타조로, 서커스단에 잡혀서 재주를 부리다가 결국 힘들어서 탈출하고 고길동의 집에 들어왔다. 허영심이 많고 공주병이 심하며, 성격도 진짜 까다롭다. 입버릇은 \"뚜아~\" 입은 또 고급이라 양주나, 파인애플, 바나나, 망고, 키위같은 과일을 좋아한다. 또치라는 이름은 얼핏 봐도 타조를 변형한 것 같지만 아니다. 아기공룡 둘리 애장판에 실린 작가의 말에 의하면 당시 딸의 애칭이 \"홍실\"이었는데 어린 딸이 홍실을 또치 비슷하게 발음하는 것을 듣고 지은 이름이라고 한다. 둘리 일당 중 또치만 암컷으로 설정한 이유를 애장판 인터뷰에서 밝혔다. 남자들만 있으면 너무 삭막한 느낌이 들기 때문이라고. 정확히는 \"남자들만의 세계, 얼마나 삭막할까요.\"라고 대답했다.";
} else {
return "아기공룡 둘리의 등장인물. 이름은 도넛에서 유래됐다. 깐따삐야 별에서 온 외계인으로, 바이올린 형태의 소형 우주선 타임 코스모스를 타고 온따삐야 별로 놀러가려다 타임 코스모스가 고장나면서 고길동의 집 앞마당에 떨어졌고, 고향으로 돌아가지 못하고 어쩔 수 없이 고길동 집에 살게 된다. 신체 특징을 보면 거북 몸에 머리가 설치류에 가깝다.[9] 특히 대포알같은 몸통 안에 팔다리와 그 큰 머리 전부를 쏘옥 집어넣어 완전한 공 모양이 될 수 있다. 이 형태를 공격 혹은 방어에 이용하는 모습이 종종 보인다. 고길동은 아예 대놓고 자라 새끼, 거북이 새끼라고 부르며, 고길동을 잡으러 가는 만화책 세계대탐험의 중국 에피소드에서는 아예 자기가 직접 자라과라고 한다. 다만, 몸에 있는 빨간 것은 사실 의복으로, 깐따삐야 별에 쳐들어 온 외계인들과 싸우는 편에서 외계인들이 옷을 벗긴 후 온종일 벗고 다니는 도우너를 볼 수 있다. 지금은 삭제된 네이버 웹툰 한국만화거장전에서 김수정 작가가 아기공룡 둘리의 새 극장판을 홍보하기 위해 올린 만화에서는 푸들로 오해받기도 했다.";
}
}
@Tool(description = "이정은 강사에 대한 정보를 제공")
public String getInfo( ) {
return "이정은강사가 아기공룡 둘리라는 만화에서 제일 좋아하는 캐릭터는 또치이다." ;
}
}
Mcpserver1Application
package com.example.mcpserver1;
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.List;
@SpringBootApplication
public class Mcpserver1Application {
public static void main(String[] args) {
SpringApplication.run(Mcpserver1Application.class, args);
}
@Bean
public List<ToolCallback> doolyTools(DoolyService doolyService) {
return List.of(ToolCallbacks.from(doolyService));
}
}
여기까지 만든 파일로 jar 파일로 만들고 서버로 만든다
1. build,gradle에 세팅을 추가
(이름 간단하게, 버전도 지정)
bootJar{
archiveBaseName = "mcpserver1"
archiveFileName = "doolymcpserver.jar"
archiveVersion = "0.0.1"
}
2. bootJar를 더블클릭해서 실행

3. 생성

4. 터미널에서 jar를 실행시킴

앞에서 커서가 깜박거리면 실행된 것
ctrl + c 누르면 종료
앞의 프로젝트 mcpclientchat 프로젝트를 복붙해서 새로 프로젝트를 만듬

mpcclientchatmyserver로 이름변경 후 인텔리제이에서 연다

복사해서

springaimcp 폴더에 넣는다
json 파일만 변경하면 된다
{
"mcpServers": {
"filesystem" :{
"command": "E:\\3\\node_js\\npx.cmd",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"E:\\daewooproject\\springaimcp"
]
},
"datetime": {
"command": "E:\\3\\node_js\\npx.cmd",
"args": [
"-y",
"@pinkpixel/datetime-mcp"
],
"env": {
"TZ": "Asia/Seoul"
}
},
"spring=ai-mcp-server": {
"command": "java",
"args": [
"-Dlogging.level.root=OFF",
"-Dspring.main.banner-mode=off",
"-jar",
"E:\\daewooproject\\springaimcp\\doolymcpserver.jar"
]
}
}
}
추가로 목록이 나오게 하려면 사진처럼 코드 추가

실행하면

목록이 뜨고

정상적으로 서버도 실행되는것을 확인할 수 있다

대화내역 지우기를 누르면 전체가 지워지며, /clear 호출이라는 로그가 뜬다

'대우개발원 수업 내용 > spring기반 ai' 카테고리의 다른 글
| spring airag 3일차 / Rag, springairag2 (0) | 2026.05.26 |
|---|---|
| spring airag 2일차 / Rag, springairag1,springairag2 (0) | 2026.05.24 |
| spring airag 1일차 / Rag, springairag1 (0) | 2026.05.21 |
| spring ai 10일차 / 다중 llm, springai5 (0) | 2026.05.20 |
| spring ai 9일차 / 다중 llm, springai5 (0) | 2026.05.19 |



