상태가 없는 구조: 대규모 언어 모델(LLM)은 근본적으로 상태가 없어(Stateless) 각 요청이 이전 상호작용과 완전히 독립적으로 처리됩니다.
대화 맥락 유실: 첫 턴에 "내 이름은 제임스 본드야"라고 말하더라도 다음 턴에 "내 이름이 뭐지?"라고 물으면 LLM은 이전 기억을 유지하지 못해 답변하지 못합니다.
별도 메커니즘의 필요성: 여러 턴에 걸쳐 자연스러운 대화를 이어가기 위해서는 대화의 맥락을 붙들어 매어줄 별도의 장치가 필수적입니다.
외부 세계와의 연계와 Memory의 포지션

정의: 사용자와 LLM 간 주고받은 대화 이력(System, User, Assistant 메시지 목록)을 일관되게 저장, 관리, 재구성하여 지속적인 대화 맥락을 유지해 주는 메모리 관리 컴포넌트입니다.
주요기능
conversationId(사용자/세션별 식별자)를 기반으로 이전 메시지들을 식별하고 체계적으로 저장합니다.인터페이스 정의: ChatMemory 인터페이스는 대화 기억을 관리하기 위해 기본 메서드들을 정의합니다.
핵심 메서드:
add(String conversationId, Message message): 유효성 검증(Assert)을 거쳐 단일 메시지를 추가하는 default 메서드입니다.add(String conversationId, List<Message> messages): 대화 이력 목록을 추가합니다.get(String conversationId): 특정 세션의 메시지 목록을 조회합니다.clear(String conversationId): 특정 세션의 대화 기억을 삭제합니다.maxMessage)는 20개(UserMessage + AssistantMessage 합산)입니다.InMemoryChatMemoryRepository 등 다양한 ChatMemoryRepository를 선택하여 이용할 수 있습니다.ChatClient.Builder 설정 시 MessageChatMemoryAdvisor에 chatMemory를 등록하여 일반 Bean처럼 자동 주입받아 사용합니다.MessageWindowChatMemory.builder().maxMessage(10).build()와 같이 수동으로 생성할 수 있습니다.// 1) 자동 주입 및 기본 사용법: Spring이 자동 구성한 chatMemory와 chatClientBuilder를 주입받아 설정
public MessageChatMemoryService(ChatMemory chatMemory, ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultAdvisors(
// 대화 맥락을 자동으로 관리하기 위해 Advisor에 chatMemory 등록
MessageChatMemoryAdvisor.builder(chatMemory).build(),
// 프롬프트 및 응답 과정을 로그로 확인하기 위한 로거 어드바이저 추가
new SimpleLoggerAdvisor()
)
.build();
}
// 2) 수동 설정: 대화 윈도우 크기를 10개로 제한하여 ChatMemory 인스턴스 직접 생성
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessage(10) // 최근 유지할 메시지 개수 설정 (UserMessage + AssistantMessage 합산)
.build();
RedisChatMemoryRepository 적용 방법 (영속성 확장)
spring-ai-starter-model-chat-memory-repository-redis 의존성을 주입합니다.@Autowired로 주입받은 RedisChatMemoryRepository를 MessageWindowChatMemory.builder()에 바인딩하여 애플리케이션 재시작 후에도 대화가 유지되도록 구성할 수 있습니다.// Redis 기반의 영속성 저장소를 주입받아 애플리케이션 재시작 후에도 대화 이력이 유지되도록 설정
@Autowired
RedisChatMemoryRepository chatMemoryRepository;
// 주입받은 Redis 저장소와 최대 메시지 수(10개)를 조합하여 MessageWindowChatMemory 인스턴스 수동 생성
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository) // 대화 데이터를 Redis에 저장/조회하도록 지정
.maxMessages(10) // 메모리에 유지할 최대 메시지 개수 설정 (User + Assistant 합산)
.build();
MessageChatMemoryAdvisor: ChatMemory에서 받은 대화 기억을 User Message와 AssistantMessage로 구분하여 생성한 뒤 프롬프트에 추가하며 LLM이 역할(Role) 기반으로 추론할 수 있도록 그대로 유지합니다.// 1. 최대 메시지 수(윈도우 크기)를 10개로 제한하여 대화를 기억하는 ChatMemory 인스턴스 생성
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessage(10)
.build();
// 2. 생성한 chatMemory를 기반으로 대화 이력을 프롬프트에 자동으로 주입하는 MessageChatMemoryAdvisor 생성
MessageChatMemoryAdvisor advisor = MessageChatMemoryAdvisor.builder(chatMemory)
// 3. ChatClient 빌더에 defaultAdvisor로 위에서 만든 advisor를 등록하여 대화 맥락이 유지되는 ChatClient 완성
ChatClient chatClient = chatClientBuilder.defaultAdvisors(advisor).build();
MessageChatMemoryAdvisor 저장 방식 및 구조
Message) 리스트로 만들어 현재 메시지에 그대로 끼워 넣는 방식을 사용합니다.Messages Array)을 표준으로 지원합니다.{
"messages": [ // 대화 기록을 별도의 메시지 객체(Message) 리스트로 만들어 현재 메시지에 끼워 넣는 방식의 JSON 구조
{
"role": "system",
"content": "..."
},
{
"role": "user",
"content": "Spring AI가 뭐야?" // 과거 User 메시지 (대화 이력)
},
{
"role": "assistant",
"content": "Spring 기반 AI 애플리케이션 개발 프레임워크입니다." // 과거 Assistant 메시지 (대화 이력)
},
{
"role": "user",
"content": "그럼 Advisor는 뭐야?" // 현재 User 메시지 (가장 마지막에 위치하여 모델이 현재 요청으로 인식)
}
]
}
Session 별 대화 기억 Index 관리
HttpSession)나 사용자 ID, Token 등을 활용할 수 있습니다.ChatMemory.CONVERSATION_ID Context Key에 고유 ID를 할당하여 Session 별 대화 기억을 유지합니다.// /default 엔드포인트로 들어오는 GET 요청을 처리하며, 사용자 질문(question)과 세션(session) 정보를 파라미터로 받음
@GetMapping("/default")
public Map<String, Object> chatMemory(@RequestParam String question, HttpSession session) {
String answer = chatClient.prompt().user(userText)
// 1. 현재 HTTP 세션의 고유 ID를 ChatMemory의 대화 ID(CONVERSATION_ID)로 전달하여 세션별 대화 맥락을 분리/유지함
.advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, session.get()))
.call()
.content(); // 응답 본문(텍스트)을 문자열로 추출
// 2. 클라이언트에게 전달할 결과 데이터를 Map 객체로 생성하여 반환 (질문, 세션 ID, 답변 포함)
return Map.of(
"question", question,
"conversationId", session.getId(),
"answer", answer);
}
Advisor를 통한 ChatClient와 ChatMemory 통합 흐름
AOP 스타일 메커니즘: ChatClient의 호출 전후에 특정 로직을 실행하는 AOP 스타일의 매커니즘을 제공합니다.

chatClient.prompt() 호출: 사용자가 보낸 질문과 함께 대화 생성을 요청하는 초기 진입점입니다.Invoke 'Advisor'): 실제 LLM에 요청이 전달되기 전 AOP 스타일의 어드바이저가 가로채어 대화 기억을 불러오는 부가 로직을 실행합니다.chatMemory.get(conversationId) 호출: 메모리 관리 컴포넌트가 세션 고유 식별자(conversationId)를 사용해 해당 사용자의 이전 대화 이력을 조회합니다.ChatMemoryRepository에서 메시지 패치 (Fetch Messages): 메모리 저장소(InMemoryChatMemoryRepository나 Redis 등)에 실제로 저장되어 있던 과거 메시지 데이터들을 읽어옵니다.UserMessage와 AssistantMessage 목록을 가져오게 됩니다.)Retrieved Messages): 저장소에서 가져온 대화 기록 목록이 어드바이저 쪽으로 전달됩니다.Augment Prompt): 가져온 과거 대화 목록을 현재 프롬프트의 메시지 배열(messages)에 그대로 끼워 넣어 LLM이 이전 맥락을 파악할 수 있도록 프롬프트를 확장합니다.Send Augmented Prompt): 과거 대화 이력과 현재 사용자 질문이 모두 포함된 완성된 프롬프트가 LLM에게 최종 전달되어 문맥에 맞는 답변을 생성하게 됩니다.애플리케이션 구성 (VectorStoreConfig)
JdbcTemplate과 EmbeddingModel을 주입받아 PgVectorStore.builder()를 통해 스키마 이름과 테이블 이름(chat_memory_vector_store)을 지정하여 설정합니다.VectorStoreChatMemoryAdvisor 사용법
VectorStoreChatMemoryAdvisor.builder(vectorStore).build() 형태로 ChatClient의 기본 어드바이저에 등록합니다..advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))를 통해 세션별 대화 ID를 전달하여 동작합니다.public class VectorStoreService {
private ChatClient chatClient;
// 1. 생성자 주입: VectorStore와 ChatClient.Builder를 받아 VectorStore 전용 ChatClient 설정
public VectorStoreService(
@Qualifier("chatMemoryVectorStore") VectorStore vectorStore,
ChatClient.Builder chatClientBuilder) {
// ChatClient 객체 생성 및 기본 어드바이저 설정
this.chatClient = chatClientBuilder
// VectorStoreChatMemoryAdvisor를 등록하여 대화 내용을 벡터로 변환/검색하도록 지정
.defaultAdvisors(VectorStoreChatMemoryAdvisor.builder(vectorStore).build(), new SimpleLoggerAdvisor())
.build();
}
// 2. 대화 요청 메서드: 사용자 입력과 세션 ID를 받아 LLM 응답 반환
public String chat(String userText, String conversationId) {
String answer = chatClient.prompt()
.user(userText) // 사용자가 입력한 현재 텍스트 전달
// 현재 대화가 속한 세션 ID를 파라미터로 전달하여 해당 세션의 기억을 관리/조회함
.advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content(); // LLM의 응답 내용을 문자열로 추출
return answer;
}
}
특성 비교 요약 (MessageChatMemoryAdvisor vs VectorStoreChatMemoryAdvisor)
| 항목 | MessageChatMemoryAdvisor | VectorStoreChatMemoryAdvisor |
|---|---|---|
| 기억 형태 | 단기 기억 (Working Memory) | 장기 기억 (Semantic Memory) |
| 저장 위치 | 다양한 ChatMemoryRepository | Vector Store (PGVector, Weaviate 등) |
| LLM 전달 방식 | 대화 원문 전체/일부를 Prompt에 삽입 | 의미 기반으로 검색된 메시지만 삽입 |
| 토큰 소비 | 많음 (대화가 길어짐) | 적음 (필요한 메시지만 전달) |
| 저장 한계 | 토큰 제한으로 오래된 내용 삭제 | 사실상 무제한 |
| 적합한 질문 | "내가 뭐라고 했지?" | "이전 주제 다시 설명해줘" |
| 활용 목적 | 대화 흐름 이어가기 | 지식 검색 / 장기 기억 |
AI 모델이 외부 시스템의 기능(Tool)을 호출해서 자신의 능력을 확장하기 위한 기술 및 패턴으로 Function Call이라고도 불립니다.

1) 도구 정의 포함: 모델이 Tool을 사용할 수 있도록 채팅 요청에 Tool 정의(Name, description, Input Param Schema)를 포함합니다.
2) 모델의 호출 요청: 모델이 Tool 호출이 필요하다고 판단하면 정의된 스키마를 기반으로 Tool 이름과 입력 변수값을 포함하는 응답을 Spring AI에 전송합니다.
3) 애플리케이션의 Tool 실행: Spring AI는 Tool 이름을 기준으로 해당 Tool을 식별하고 전달받은 입력 파라미터로 Tool을 실행합니다. (예: getCurrentWeather 메서드 실행)
4) 실행 결과 전달: Tool의 실행 결과를 다시 모델에게 전달합니다.
5) 최종 답변 생성: 모델은 Tool 실행 결과를 추가 컨텍스트로 사용하여 최종 답변을 생성합니다. (예: "오늘 날씨가 14도로 추워요 감기 조심하세요" 답변 생성)
| 구분 | LLM (AI Model) | Spring AI (애플리케이션) |
|---|---|---|
| 역할 및 책임 | 추론 및 판단, 도구 호출 요청 | 도구 정의 제공, 실제 도구 실행 및 중계 |
| 수행 작업 | • 스스로 답할 수 없는 질문인지 인지하고 필요한 Tool 호출 요청 • 필요한 매개 변수(Input Param)를 추론하여 전달 • Tool 실행 결과를 컨텍스트로 받아 최종 답변 생성 | • 도구(Tool)들을 정의하고 목록을 LLM에게 노출/전달 • LLM이 보낸 도구 호출 요청을 수신 및 메서드로 매핑 • 전달받은 입력 파라미터로 실제 외부 기능(메서드 등)을 실행 |
Spring AI Tool 정의 및 구현 방법
@Tool 어노테이션name(도구의 이름, 생략 시 메서드명 사용)과 description(LLM이 도구의 기능을 이해하도록 돕는 상세 설명)을 지정할 수 있습니다.@ToolParam 어노테이션description(파라미터에 대한 설명)과 required(필수 여부, 기본값 true)를 설정할 수 있습니다.@Tool(description = "도시 이름으로 현재 날씨를 조회합니다. 예: Seoul")
public String getCurrentWeather(
@ToolParam(description = "도시 이름 (예: Seoul)", required = true) String city) {
시간 정보 검색 도구(DateTimeTools) 구현 예제 코드
@Component // 1. 스프링 빈으로 등록하여 관리되도록 설정
@Slf4j
public class DateTimeTools {
// 2. LLM이 이 메서드를 어떤 도구인지 인식할 수 있도록 @Tool 설명 추가
@Tool(description = "사용자의 시간대에 맞는 현재 날짜와 시간 정보를 제공합니다.")
public String getCurrentDateTime(
// 3. LLM이 어떤 입력값을 넣어야 하는지 알 수 있도록 파라미터 설명 추가
@ToolParam(description = "지역명 (예: Asia/Seoul)", required = true) String regionName) {
// 전달받은 지역명(ZoneId)을 기반으로 현재 시간 계산
String nowTime = ZonedDateTime.now(ZoneId.of(regionName)).toString();
log.info("현재 시간 조회: region={}, time={}", regionName, nowTime);
return nowTime;
}
}
ChatClient에 Tool 등록 및 사용 방법
@Service
public class ChatService {
private final ChatClient chatClient;
// 생성자 주입을 통해 ChatClient와 직접 만든 도구(DateTimeTools 등)들을 연동
public ChatService(ChatClient.Builder chatClientBuilder,
DateTimeTools dateTimeTools,
FileSystemTool fileSystemTool,
WeatherTools weatherTools) {
this.chatClient = chatClientBuilder
// .tools() 메서드를 통해 애플리케이션이 가진 도구 객체들을 등록
// Spring AI가 메서드의 메타데이터를 Tool Definition 형태로 LLM에 전달함
.defaultTools(dateTimeTools, fileSystemTool, weatherTools)
.build();
}
public String chat(String request, ChatMemory session) {
return this.chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, session.getId()))
.user(request)
.call()
.content(); // LLM이 필요 시 도구를 호출하고, 그 결과를 바탕으로 최종 답변 반환
}
}
.tools() 메서드 호출: chatClient.prompt() 체이닝 과정에서 .tools(dateTimeTools, fileSystemTool, weatherTools)와 같이 작성하여 구현한 도구 객체들을 등록합니다.Step 1. Spring AI → LLM (Tool Definition 전달)
스프링 애플리케이션이 실행될 때 @Tool과 @ToolParam으로 정의된 메서드 메타데이터를 파싱하여 ToolDefinition 객체를 만들고 사용자 질문과 함께 LLM에 JSON 형태로 전달합니다.
// [Spring AI 내부 개념] LLM에게 전달될 ToolDefinition의 구조
public final class ToolDefinition {
private final String name; // LLM에게 알려주는 메서드 이름 (예: getCurrentWeather)
private final String description; // LLM이 이해할 수 있는 메서드 설명 ("도시의 현재 기온을 조회한다")
private Map<String, Object> inputSchema; // 메서드 파라미터 이름, 타입 스키마 정보 (city: string)
}
Step 2. LLM → Spring AI (Tool Request 응답)
사용자가 "부산 현재 기온 알려줘"라고 질문하면 LLM은 보유한 도구 중 getCurrentWeather가 적합하다고 판단하여 해당 도구를 arguments와 함께 호출해달라고 요청(Assistant Message)합니다.
// LLM이 Spring AI로 보내는 tool_calls 응답 예시
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "getCurrentWeather",
"arguments": "{\"city\":\"Busan\"}" // LLM이 추출한 파라미터 값
}
}
]
}
Step 3. Spring AI 내부 Tool 메서드 실행
Spring AI는 LLM이 보낸 도구 이름과 인자값을 받아 일치하는 맵핑 메서드를 찾아 Java Reflection을 통해 실제로 실행합니다.
@Component
public class WeatherTool {
@Tool(description = "도시 이름으로 현재 날씨를 조회합니다. 예: Seoul")
public String getCurrentWeather(
@ToolParam(description = "도시 이름 (예: Seoul)", required = true) String city) {
// 1. LLM이 넘겨준 city("Busan") 파라미터를 받아 로직 수행
log.info("날씨 조회 요청 - 도시: {}", city);
// 2. 외부 API 호출 또는 내부 비즈니스 로직 실행
String result = "{\"temp\": 14}";
// 3. 실행 결과를 반환하면 Spring AI가 이를 가로채서 처리함
return result;
}
}
Step 4 ~ 5. Spring AI → LLM (ToolResponseMessage 전달) 및 최종 답변
메서드 실행 결과값을 ToolResponseMessage(role: "tool") 형태로 포장하여 다시 LLM에 전달하고 LLM은 이 데이터를 바탕으로 사용자에게 자연스러운 최종 답변을 생성합니다.
// 4단계: Spring AI가 LLM에게 전달하는 도구 실행 결과 메시지
{
"role": "tool",
"tool_call_id": "call_1", // Step 2의 id와 매칭됨
"name": "getCurrentWeather",
"content": "{\"temp\":14}"
}
// 5단계: LLM이 최종적으로 사용자에게 반환하는 자연어 답변
{
"role": "assistant",
"content": "부산의 현재 기온은 14°C 입니다."
}