Chat Memeory란?
보통 AI 모델에 질의할 때, 이전에 보냈던 메시지를 참고하여 답변하는 것을 보고는 하는데요, AI 모델이 이전 메시지를 참고하기 위해서는 메시지를 보관하기 위한 장치가 필요하고, 이를 메시지시 전송 부 앞 프롬프트에 추가해야 합니다.
Chat Memory란, 이전 메시지 맥락을 기억하기 위해 제공하는 기능으로 Chat Memory를 통해 AI 모델에 질의할 때 이전 메시지 기반으로 조금 더 관련성 있는 응답을 받을 수 있다는 특징을 가지고 있습니다.
오늘은 Spring AI에서 이전 대화 맥락을 기억하기 위해 제공하는 Chat Memroy에 대해서 알아보려고 합니다.
ChatMemoryRepository
Chat Memory를 사용하면서 가장 많이 접하게 될 Interface로, Spring AI에서 이를 구현한 기본 구현체로는 InMemoryChatMemoryRepository를 제공하고 있습니다.
개인적으로는, 우리가 MVC를 구현할 때 DAO에 해당하는 Repository 계층에서 사용할 수 있다고 생각이 드네요.
해당 Repository를 구현하여 Jdbc를 사용하는 저장소와도 연결할 수 있다는 특징을 가지고 있습니다.
public interface ChatMemoryRepository {
List<String> findConversationIds();
List<Message> findByConversationId(String conversationId);
void saveAll(String conversationId, List<Message> messages);
void deleteByConversationId(String conversationId);
}
ChatMemory
공식 문서에 따르면, Chat Memory에서는 단순히 ChatMemoryRepository를 구현하여 사용하는 것이 아닌, 다음과 같은 ChatMemory 인터페이스를 구현한 MessageWindowChatMemory의 생성자로 넣어서 사용하라고 가이드 되어있습니다.
public final class MessageWindowChatMemory implements ChatMemory {
private static final int DEFAULT_MAX_MESSAGES = 20;
private final ChatMemoryRepository chatMemoryRepository;
private final int maxMessages;
private MessageWindowChatMemory(ChatMemoryRepository chatMemoryRepository, int maxMessages) {
Assert.notNull(chatMemoryRepository, "chatMemoryRepository cannot be null");
Assert.isTrue(maxMessages > 0, "maxMessages must be greater than 0");
this.chatMemoryRepository = chatMemoryRepository;
this.maxMessages = maxMessages;
}
...
}
사용 예시
MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
.maxMessages(10)
.build();
보통은 MVC 방식을 사용할 때, Repository를 Spring Bean으로 등록해서 사용하는데, ChatMemory라는 계층이 중간에 끼어든 것으로 보아, Spring AI에서 지향하는 방식은 다음과 같이 Repository 계층과 Advisor 계층이 분리되는 방식을 지향했던 것 같습니다.
Controller → Service → ChatClient
↳ (Advisor) ChatMemory → ChatRepository
하지만, MessageWindowChatMemory는 Spring Bean으로 등록되지 않아, 직접 생성자에 ChatMemoryRepository를 주입해야하는데요, 이 방식이 개인적으로는 Spring 지향적으로 사용될 수 없다고 생각되기 때문에, 만약 사용하게 된다면 Bean으로 등록하여 사용하거나 ChatMemrory를 구현한 클래스에 @Component 어노테이션을 붙여서 사용할듯 합니다.
In-Memory Repository
Spring AI의 ChatMemoryRepository 인터페이스를 구현한 기본 Repository입니다.
다음과 같이, 내부적으로 CurrentHashMap을 사용하여, 서버 내에서 메모리에 conversationID를 key로 가지는 메시지를 기록하는 방식입니다.
public final class InMemoryChatMemoryRepository implements ChatMemoryRepository {
Map<String, List<Message>> chatMemoryStore = new ConcurrentHashMap();
public InMemoryChatMemoryRepository() {
}
public List<String> findConversationIds() {
return new ArrayList(this.chatMemoryStore.keySet());
}
public List<Message> findByConversationId(String conversationId) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
List<Message> messages = (List)this.chatMemoryStore.get(conversationId);
return (List<Message>)(messages != null ? new ArrayList(messages) : List.of());
}
public void saveAll(String conversationId, List<Message> messages) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
Assert.notNull(messages, "messages cannot be null");
Assert.noNullElements(messages, "messages cannot contain null elements");
this.chatMemoryStore.put(conversationId, messages);
}
public void deleteByConversationId(String conversationId) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
this.chatMemoryStore.remove(conversationId);
}
}
JdbcChatMemoryRepository
Chat Memory로 사용할 수 있는 두 번째 Repository는 JDBC를 이용한 Repository입니다.
의존성 정보
dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc'
}
해당 Repository는 JDBC를 사용하기 때문에 다음 생성자에 JdbcTemplate과 JdbcChatMemoryRepositoryDialect, PlatformTransactionManager를 필요로 합니다.
public final class JdbcChatMemoryRepository implements ChatMemoryRepository {
private final JdbcTemplate jdbcTemplate;
private final TransactionTemplate transactionTemplate;
private final JdbcChatMemoryRepositoryDialect dialect;
private static final Logger logger = LoggerFactory.getLogger(JdbcChatMemoryRepository.class);
private JdbcChatMemoryRepository(JdbcTemplate jdbcTemplate, JdbcChatMemoryRepositoryDialect dialect, PlatformTransactionManager txManager) {
Assert.notNull(jdbcTemplate, "jdbcTemplate cannot be null");
Assert.notNull(dialect, "dialect cannot be null");
this.jdbcTemplate = jdbcTemplate;
this.dialect = dialect;
this.transactionTemplate = new TransactionTemplate((PlatformTransactionManager)(txManager != null ? txManager : new DataSourceTransactionManager(jdbcTemplate.getDataSource())));
}
...
}
JdbcChatMemoryRepositoryDialect의 경우, 다음과 같은 인테페이스를 제공하는데요, 현재 Spring AI에서 제공하는 RDB 관련 기본 구현체는 다음과 같습니다.
- HsqldbChatMemoryRepositoryDialect => HSQL
- MysqlChatMemoryRepositoryDialect => MySQL
- PostgresChatMemoryRepositoryDialect => PostgreSQL
- SqlServerChatMemoryRepositoryDialect => MSSQL / SQL Server
public interface JdbcChatMemoryRepositoryDialect {
String getSelectMessagesSql();
String getInsertMessageSql();
String getSelectConversationIdsSql();
String getDeleteMessagesSql();
static JdbcChatMemoryRepositoryDialect from(DataSource dataSource) {
try {
String url = dataSource.getConnection().getMetaData().getURL().toLowerCase();
if (url.contains("postgresql")) {
return new PostgresChatMemoryRepositoryDialect();
}
if (url.contains("mysql")) {
return new MysqlChatMemoryRepositoryDialect();
}
if (url.contains("mariadb")) {
return new MysqlChatMemoryRepositoryDialect();
}
if (url.contains("sqlserver")) {
return new SqlServerChatMemoryRepositoryDialect();
}
if (url.contains("hsqldb")) {
return new HsqldbChatMemoryRepositoryDialect();
}
} catch (Exception var2) {
}
return new PostgresChatMemoryRepositoryDialect();
}
}
JPA도 되나?
현재 Spring AI에서는 해당 Repository를 지원하고 있지 않기 때문에 JPA를 사용한다면 직접 구현이 필요할듯 하네요.
다만, 아래 Dialect 구현체를 볼 때, 기본적으로 conversation을 저장하거나 조회하는 쿼리를 간단하게 제공하는 것으로 보아, 굳이 JPA로 구현할 필요는 없어 보입니다.
public class MysqlChatMemoryRepositoryDialect implements JdbcChatMemoryRepositoryDialect {
public MysqlChatMemoryRepositoryDialect() {
}
public String getSelectMessagesSql() {
return "SELECT content, type FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY `timestamp`";
}
public String getInsertMessageSql() {
return "INSERT INTO SPRING_AI_CHAT_MEMORY (conversation_id, content, type, `timestamp`) VALUES (?, ?, ?, ?)";
}
public String getSelectConversationIdsSql() {
return "SELECT DISTINCT conversation_id FROM SPRING_AI_CHAT_MEMORY";
}
public String getDeleteMessagesSql() {
return "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ?";
}
}
Scheme 지원
JDBC 의존성을 사용하게 되면, 다음과 같이 기본적으로 제공하는 RDB(HSQL, MySQL, PostgreSQL, SQL Server)에 대한 Scheme.sql을 제공해줍니다.
scheme-mariadb.sql 예시
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
conversation_id VARCHAR(36) NOT NULL,
content TEXT NOT NULL,
type VARCHAR(10) NOT NULL,
`timestamp` TIMESTAMP NOT NULL,
CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
);
CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX
ON SPRING_AI_CHAT_MEMORY(conversation_id, `timestamp`);
그 외 Repository
기본 제공인 InMemoryChatMemoryRepository와 JdbcChatMemoryRepository는 Chat Memory를 사용할 때 필수적이지만, 다음 두 Repository는 환경에 따라 확장성있는 Repository 구축 시에 필요할듯 하고 해당 저장소의 특징을 제외하면 사용법은 비슷하여 간단하게 넘어가도록 하겠습니다.
CassandraChatMemoryRepository
Apache Casandra를 적용한 Repository로, Chat Memory로 사용 시에 다음과 같은 이점을 가질 수 있습니다.
- 단순 메모리 저장소나 RDB로는 처리하기 어려운 대규모 대화 데이터 저장 및 확장성 관리
- 시간 순서로 누적되는 대화 로그를 영속적, 안정적으로 유지
- TTL 같은 기능으로 오래된 데이터를 자동 삭제 가능
- 시스템 장애/노드 장애에도 대화 로그 유지 (고가용성)
Neo4j ChatMemoryRepository
그래프 DB (property graph database)로 메시지와 관계를 노드와 관계로 저장하는 구현체입니다. 해당 Repository 사용 시에 다음과 같은 이점을 가질 수 있습니다.
- 관계 중심 탐색: 대화 메시지들을 노드와 관계로 표현할 수 있으니까, “어떤 메시지와 연결되어 있는가”, “어떤 대화 흐름이 분기했는가” 등을 탐색하기 수월합니다.
- 그래프 쿼리의 유연성: Cypher 쿼리로 복잡한 관계 패턴 검색 가능—for example, “이 메시지와 유사한 문맥의 과거 대화 흐름”을 그래프 패턴 매칭으로 찾기 용이합니다.
- 세션/메시지 구조 분리: 문서상 Session 노드, Message 노드, ToolCall, Media, Metadata 등 다양한 노드 레이블을 지원하니까 구조화된 저장이 가능합니다.
- 자동 인덱스 지원: conversation ID나 메시지 인덱싱 등에 대해 Neo4j가 인덱스를 자동으로 걸어준다는 게 문서상 언급돼 있습니다.
- 스키마 유연성: 스키마초기화 요구가 적고, 노드/관계 중심 구조는 유연하게 확장 가능성이 있습니다.
Chat Memory를 사용 시 주의할 요소
Tool 호출과 메모리와의 관계 => 호출 간에 따로 저장하지는 않는다고 합니다.
예를들어, 예를 들어 다음과 같은 흐름이 있다고 할 때,
- 사용자: “서울 날씨 알려줘”
- 모델 내부 → 도구(예: 외부 날씨 API 호출)
- 모델 응답: “오늘 서울은 맑음입니다.”
여기서 “모델 내부 → 도구 호출 → 응답 가져오기” 사이에 메시지 교환이 있을 수 있는데 (예: 요청 파라미터, API 응답, 변환 로직 등),
그런 메시지는 기본 Advisor + ChatMemory 구조에서는 저장되지 않는다고 합니다.
따라서, 나중에 대화를 복기할 때, "도구 호출 세부 내용"이 누락될 수 있다고 합니다.
ChatMemoryAdvisor란 무엇인가?
ChatMemory를 통해 이전 맥락을 어떻게 저장할 것인지에 대해 ChatMemoryRepository와 ChatMemory 인터페이스에 대해 알아봤는데요, 이전에 Advisor에 대해 알아본 바와 같이, AI 모델에 요청하기 전 무언가 필요한 작업을 하는 역할을 하는 것이 바로 Advisor입니다.
Chat Memory에서도 제공하는 Advisor가 존재하는데요, MessageChatMemoryAdvisor, PromptChatMemoryAdvisor, VectorStoreChatMemoryAdvisor 이렇게 세 종류의 ChatMemoryAdvisor가 존재하고, 세 구현체는 BaseAdvisor를 구현한 BaseChatMemoryAdvisor 인터페이스를 구현하고 있습니다. 각각의 Advisor는 AI 모델에 요청하기 전 Prompt에 메시지 맥락을 어떻게 넣어서 전달할지를 결정하는 역할을 합니다.
public interface BaseChatMemoryAdvisor extends BaseAdvisor {
default String getConversationId(Map<String, Object> context, String defaultConversationId) {
Assert.notNull(context, "context cannot be null");
Assert.noNullElements(context.keySet().toArray(), "context cannot contain null keys");
Assert.hasText(defaultConversationId, "defaultConversationId cannot be null or empty");
return context.containsKey("chat_memory_conversation_id") ? context.get("chat_memory_conversation_id").toString() : defaultConversationId;
}
}
MessageChatMemoryAdvisor
Spring AI의 기본 구현체로, 프롬프트에 메시지를 반영할 때 다음과 같이, Message 자체를 담아서 AI 모델에 전달한다는 특징을 가지고 있습니다.
[
{"role": "user", "content": "안녕?"},
{"role": "assistant", "content": "안녕하세요!"}
]
해당 방식의 특징으로는 역할(role) 정보(user, assistant, system)가 그대로 유지될 수 있고, LLM이 채팅 맥락을 구조적으로 이해할 수 있지만 토큰 수가 길어진다는 단점이 있습니다.
MessageChatMemoryAdvisor는 다음과 같은 생성자 정보를 가지고 있습니다.
public final class MessageChatMemoryAdvisor implements BaseChatMemoryAdvisor {
private final ChatMemory chatMemory;
private final String defaultConversationId;
private final int order;
private final Scheduler scheduler;
private MessageChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int order, Scheduler scheduler) {
Assert.notNull(chatMemory, "chatMemory cannot be null");
Assert.hasText(defaultConversationId, "defaultConversationId cannot be null or empty");
Assert.notNull(scheduler, "scheduler cannot be null");
this.chatMemory = chatMemory;
this.defaultConversationId = defaultConversationId;
this.order = order;
this.scheduler = scheduler;
}
...
}
PromptChatMemoryAdvisor
Spring AI의 기본 구현체로, 매 상호작용(interaction) 시 메모리에서 대화 이력을 조회하고, 이를 system 메시지(system prompt) 쪽에 다음과 같이 plain 텍스트로 덧붙이는 방식으로 컨텍스트를 제공해줍니다.
대화 기록:
User: 안녕?
Assistant: 안녕하세요!
해당 방식의 경우, 단순 문자열이므로 모델이 구조적 role 정보를 잃게 될 수 있지만 텍스트 병합이기 때문에 구현이 단순하고 유연하다는 특징을 가지고 있습니다.
PromptChatMemoryAdvisor는 다음과 같이 생성자 호출에 PromptTemplate을 넣도록 해주는데요, 이를 통해 사용자 정의 템플릿을 통해 커스터마이징한 Prompt를 넘겨줄 수 있다는 특징을 가지고 있습니다.
public final class PromptChatMemoryAdvisor implements BaseChatMemoryAdvisor {
private static final Logger logger = LoggerFactory.getLogger(PromptChatMemoryAdvisor.class);
private static final PromptTemplate DEFAULT_SYSTEM_PROMPT_TEMPLATE = new PromptTemplate("{instructions}\n\nUse the conversation memory from the MEMORY section to provide accurate answers.\n\n---------------------\nMEMORY:\n{memory}\n---------------------\n\n");
private final PromptTemplate systemPromptTemplate;
private final String defaultConversationId;
private final int order;
private final Scheduler scheduler;
private final ChatMemory chatMemory;
private PromptChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int order, Scheduler scheduler, PromptTemplate systemPromptTemplate) {
Assert.notNull(chatMemory, "chatMemory cannot be null");
Assert.hasText(defaultConversationId, "defaultConversationId cannot be null or empty");
Assert.notNull(scheduler, "scheduler cannot be null");
Assert.notNull(systemPromptTemplate, "systemPromptTemplate cannot be null");
this.chatMemory = chatMemory;
this.defaultConversationId = defaultConversationId;
this.order = order;
this.scheduler = scheduler;
this.systemPromptTemplate = systemPromptTemplate;
}
...
}
사용 예시 (Kotlin)
val chatMemory: ChatMemory = MessageWindowChatMemory(chatRepository, maxMessages = 20)
val advisor = PromptChatMemoryAdvisor.builder(chatMemory)
.promptTemplate(myCustomPromptTemplate)
.build()
val chatClient = chatClientBuilder
.defaultAdvisors(advisor)
.build()
VectorStoreChatMemoryAdvisor
다음 문서에 따르면, VectorStoreChatMemoryAdvisor의 경우, VectorStore에 해당하는 Advisor입니다.
해당 Advisor가 왜 위 두 기본 ChatMemoryAdvisor와 같이 문서에 설명 되었는지 잘 이해가 되지 않는데요, 위 두 Advisor와는 달리 기본 VectorStore을 사용하기 위해서는 다음 의존성이 필요합니다.
Spring AI Vector 의존성
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
VectorStoreChatMemoryAdvisor는 VectorStore를 기반으로 메모리를 조회하고, 그 결과를 system 메시지 쪽에 plain 텍스트로 덧붙이는 방식을 지원합니다.
해당 방식은 대화 맥락을 보존하는 대신, 대화 이력 전체를 저장하는 대신 벡터 임베딩 기반 유사도 검색을 통해 “관련된 과거 대화 조각만”을 프롬프트에 추가하는 방식을 의미하는데요, 문서상에는 기본 템플릿을 사용해서 system prompt + 검색된 메모리를 합치는 방식도 지원하고, .promptTemplate() 같은 옵션으로 사용자 정의 템플릿을 줄 수 있다고 나와 있기는 합니다. => 관련 문서
참고
https://docs.spring.io/spring-ai/reference/api/chat-memory.html
Chat Memory :: Spring AI Reference
Spring AI auto-configures a ChatMemory bean that you can use directly in your application. By default, it uses an in-memory repository to store messages (InMemoryChatMemoryRepository) and a MessageWindowChatMemory implementation to manage the conversation
docs.spring.io
'Spring Framework > AI' 카테고리의 다른 글
Spring AI - Advisor API (0) | 2025.09.30 |
---|---|
Spring AI - Structured Output Converter (0) | 2025.09.27 |
Spring AI - ChatClient API 살펴보기 (0) | 2025.09.21 |
Spring AI - Prompt와 Message (0) | 2025.09.21 |
Spring AI - Chat Client API 시작하기 (0) | 2025.09.20 |