26/07/29 IL(I Learned) - Spring AI 2 (1)

Let's take a break·2026년 7월 30일

MyBatis를 활용한 AI Chat Memory 관리와 데이터 접근 계층 구현

Spring AI의 Chat Memory 기능을 MyBatis와 연동하여 AI와 사용자의 대화 내용을 데이터베이스에 저장하고, 이전 대화 내용을 다시 불러와 대화를 이어갈 수 있는 기능을 구현하였다.

기존에는 AI와의 대화가 한 번의 요청과 응답으로 끝나는 구조였다면, 이번에는 사용자의 대화 기록을 데이터베이스에 영속적으로 저장(Persistence) 하여 이전 대화를 기억하는 AI 서비스를 구현하는 방법을 학습하였다.

이를 위해 MyBatis를 이용하여 SQL을 직접 작성하고, Mapper 인터페이스를 통해 데이터베이스와 통신하는 구조를 설계하였다. 또한 Spring AI에서 제공하는 ChatMemoryRepository 인터페이스를 직접 구현하여 Spring AI와 MyBatis를 연결하는 방법도 익힐 수 있었다.


Chat Memory의 개념 이해

가장 먼저 이해해야 했던 개념은 Chat Memory였다.

일반적으로 생성형 AI는 사용자가 질문을 보내면 그 질문에 대해서만 답변을 생성한다.

예를 들어

사용자 : 나는 백엔드 개발자야.

AI : 반갑습니다.

-------------------------

사용자 : 내가 방금 무슨 직업이라고 했지?

AI : 알 수 없습니다.

이처럼 이전 대화를 기억하지 못한다.

하지만 Chat Memory를 사용하면 이전 대화를 저장해 두었다가 다음 요청 시 함께 AI에게 전달한다.

사용자 : 나는 백엔드 개발자야.

AI : 반갑습니다.

-------------------------

사용자 : 내가 방금 무슨 직업이라고 했지?

AI : 백엔드 개발자라고 말씀하셨습니다.

즉, Chat Memory는 AI가 기억력이 생긴 것처럼 보이도록 이전 대화를 함께 전달하는 기능이라는 점을 이해하였다.


Spring AI의 ChatMemoryRepository 구현

Spring AI에서 제공하는 ChatMemoryRepository를 직접 구현하였다.

@Repository
@RequiredArgsConstructor
public class MyBatisChatMemoryRepository
        implements ChatMemoryRepository

Spring AI는 Chat Memory를 어디에 저장할지 정해져 있지 않다.

대신

  • InMemory
  • Redis
  • MySQL
  • PostgreSQL

등 다양한 저장소를 사용할 수 있도록 ChatMemoryRepository 인터페이스를 제공한다.

이번 프로젝트에서는 MyBatis를 이용하여 MySQL에 저장하기 위해 직접 구현체를 작성하였다.

이를 통해 Spring AI의 인터페이스를 구현하여 원하는 저장 방식을 자유롭게 만들 수 있다는 점을 학습하였다.


Mapper를 이용한 데이터 접근

MyBatis는 Repository가 SQL을 직접 실행하지 않는다.

반드시 Mapper를 통해 SQL을 실행한다.

@Mapper
public interface ChatMessageMapper {

    List<String> findConversationIds();

    List<ChatMessageMyBatis> findByConversationId(String conversationId);

    void insertAll(List<ChatMessageMyBatis> messages);

    void deleteByConversationId(String conversationId);

}

Mapper 인터페이스는 SQL과 Java를 연결해 주는 역할을 한다.

이번 프로젝트에서는

  • 저장된 대화 목록 조회
  • 특정 대화 조회
  • 여러 메시지 저장
  • 특정 대화 삭제

기능을 Mapper에 정의하였다.

실제 SQL은 XML Mapper에서 작성되고, Java에서는 메서드만 호출하면 SQL이 실행되는 구조라는 점을 이해하였다.

이를 통해 SQL과 Java 코드를 분리하는 MyBatis의 구조를 학습하였다.


Conversation ID를 이용한 대화 구분

이번 프로젝트에서는 여러 사용자의 대화를 구분하기 위해 Conversation ID를 사용하였다.

public record ChatDTO(
        String message,
        String conversationId
)

Controller에서는

dto.withID(session.getId())

를 이용하여 현재 세션 ID를 Conversation ID로 사용하였다.

사용자가 여러 명일 경우 모든 대화를 하나의 테이블에 저장하면 어떤 메시지가 누구의 것인지 구분할 수 없다.

따라서 세션 ID를 Conversation ID로 사용하여

conversation_id

AAA111

AAA111

AAA111

BBB222

BBB222

CCC333

처럼 사용자별 대화를 구분하였다.

이를 통해 AI 서비스에서도 사용자 식별이 매우 중요하다는 점을 학습하였다.


Message 객체와 Entity 변환

Spring AI는 Message 객체를 사용하지만

MyBatis는 Entity를 저장한다.

따라서 두 객체를 서로 변환하는 과정이 필요하였다.

public static ChatMessageMyBatis fromMessage(
        Message message,
        String conversationId,
        int seq
)

그리고

public Message toMessage()

AI는 Message 객체를 사용한다.

하지만 데이터베이스에는 객체를 그대로 저장할 수 없기 때문에

conversation_id

message_type

content

seq

형태로 저장하였다.

필요할 때 다시 Message 객체로 변환하여 Spring AI가 사용할 수 있도록 구현하였다.

이를 통해 객체와 데이터베이스 간의 변환 과정을 직접 구현하는 방법을 학습하였다.


saveAll() 동작 원리

가장 중요한 로직은 saveAll()이었다.

chatMessageMapper.deleteByConversationId(conversationId);

...

chatMessageMapper.insertAll(chatMessages);

새로운 메시지가 저장될 때

기존 데이터를 UPDATE 하는 것이 아니라

기존 대화를 모두 삭제

현재 메모리에 있는 대화를 모두 INSERT

하는 방식을 사용하였다.

예를 들어

USER

안녕

ASSISTANT

안녕하세요.

가 저장되어 있다가

새로운 질문

오늘 날씨 어때?

가 들어오면

기존 데이터를 삭제한 뒤

안녕

안녕하세요

오늘 날씨 어때?

맑습니다.

전체를 다시 저장한다.

이 방식은 코드가 단순하며 MessageWindowChatMemory와 잘 동작하도록 설계되어 있다는 점을 이해하였다.


MessageWindowChatMemory

이번 프로젝트에서는 모든 대화를 저장하지 않고 최근 메시지만 기억하도록 설정하였다.

MessageWindowChatMemory.builder()

.chatMemoryRepository(chatMemoryRepository)

.maxMessages(3)

maxMessages(3)은

최근 3개의 메시지만 AI에게 전달한다.

예를 들어

1

안녕

2

반가워

3

점심 추천해줘

4

김치찌개 추천

5

오늘 날씨는?

라면

AI는

점심 추천해줘

김치찌개 추천

오늘 날씨는?

만 전달받는다.

이를 통해 너무 많은 대화를 보내지 않아

  • 응답 속도 향상
  • 토큰 절약
  • API 비용 감소

효과를 얻을 수 있다는 점을 학습하였다.


ChatClient와 Chat Memory 연결

MyBatis로 구현한 Chat Memory는 ChatClient에 연결하여 사용하였다.

@Bean

public ChatClient mybatisChatClient(

ChatModel chatModel,

@Qualifier("mybatisChatMemory")
ChatMemory memory
)

그리고

.advisors(
a -> a.param(
ChatMemory.CONVERSATION_ID,
dto.conversationId()
))

를 이용하여 현재 대화 ID를 전달하였다.

Conversation ID를 Advisor에 전달하면

Spring AI는

Conversation ID로 이전 대화를 조회하고

Prompt 앞에 이전 대화를 붙인 뒤

AI에게 함께 전송한다.

즉, 개발자가 직접 이전 대화를 Prompt에 이어붙이지 않아도 Spring AI가 자동으로 Chat Memory를 활용한다는 점을 학습하였다.


새롭게 학습한 내용

학습 내용세부 내용
MyBatis MapperMapper 인터페이스를 통해 SQL과 Java를 연결하는 구조
ChatMemoryRepositorySpring AI 인터페이스를 직접 구현하여 저장소를 개발하는 방법
Conversation ID사용자별 대화를 구분하여 여러 사용자의 Chat Memory를 관리하는 방법
Entity 변환Spring AI Message 객체와 DB Entity 간의 상호 변환 과정
MessageWindowChatMemory최근 대화만 유지하여 토큰 사용량과 비용을 절감하는 방법
ChatClient AdvisorConversation ID를 전달하여 이전 대화를 자동으로 불러오는 방법
트랜잭션 처리여러 메시지를 하나의 작업 단위로 저장하고 삭제하는 과정에서 데이터의 일관성을 유지하는 방법

0개의 댓글