포스트

Spring AI 멀티턴 구현: ChatMemory, Advisor

Spring AI 멀티턴 구현: ChatMemory, Advisor

LLM API는 기본적으로 이전 요청을 기억하지 않는다. 같은 채팅 화면에서 연속으로 질문하더라도, 다음 요청에 과거 메시지를 포함하지 않으면 모델은 앞선 대화를 알 수 없다.

멀티턴 대화를 구현하려면 대화 내용을 저장하고, 다음 요청의 Prompt에 다시 포함하는 과정이 필요하다. Spring AI에서는 ChatMemory, ChatMemoryRepository, MessageChatMemoryAdvisor를 조합해 이 과정을 처리할 수 있다.

이번 글에서는 Spring AI로 가장 단순한 멀티턴 대화를 구현하고, 운영 환경에서 확인해야 할 부분을 정리한다.

이 글의 예제는 원문 작성일인 2026-05-15 당시 최신 안정 버전이었던 Spring AI 1.1.6을 기준으로 작성했다.


I. LLM은 이전 대화를 기억하지 않는다

다음과 같이 두 번의 요청을 보낸다고 가정해보자.

1
2
1턴: "내 이름은 지크야"
2턴: "내 이름이 뭐였지?"

두 번째 요청에 첫 번째 대화를 포함하지 않으면 모델은 이름을 알 수 없다. LLM API 관점에서는 각 요청이 서로 독립적이기 때문이다.

멀티턴 대화는 모델 내부에 상태를 만드는 기능이 아니다. 애플리케이션이 같은 대화의 과거 메시지를 보관했다가 매 요청마다 다시 전달하는 방식이다.

결국 구현에서 결정해야 하는 것은 세 가지다.

  1. 어떤 메시지를 기억할 것인가?
  2. 메시지를 어디에 저장할 것인가?
  3. 언제 조회하고 다시 저장할 것인가?

II. Spring AI의 멀티턴 구성

Spring AI는 각 책임을 다음 컴포넌트로 분리한다.

컴포넌트책임대표 구현
ChatMemory유지할 메시지와 제거 시점을 결정MessageWindowChatMemory
ChatMemoryRepository메시지를 실제 저장하고 조회InMemoryChatMemoryRepository, JdbcChatMemoryRepository
Chat Memory Advisor모델 호출 전후에 메모리를 자동 적용MessageChatMemoryAdvisor

1. ChatMemory

ChatMemory는 대화 문맥을 다루는 상위 추상화다. conversationId를 기준으로 메시지를 추가하고, 조회하고, 제거하는 API를 제공한다.

기본 구현인 MessageWindowChatMemory는 최근 N개의 메시지를 유지한다. 기본 크기는 20개이며, 범위를 넘으면 오래된 메시지부터 제거하되 SystemMessage는 보존한다.

2. ChatMemoryRepository

ChatMemoryRepository는 보존 정책을 판단하지 않고 저장과 조회만 담당한다. 개발 단계에서는 메모리 기반 저장소를 사용할 수 있고, 운영 환경에서는 JDBC나 MongoDB 같은 영속 저장소로 교체할 수 있다.

3. MessageChatMemoryAdvisor

MessageChatMemoryAdvisorChatClient 호출 전후에 다음 작업을 수행한다.

1
2
호출 전: 이전 메시지 조회 → 현재 Prompt 앞에 추가 → UserMessage 저장
호출 후: AssistantMessage를 같은 conversationId에 저장

애플리케이션 코드는 메모리를 직접 조회하고 Prompt를 조립하지 않아도 된다. 요청마다 올바른 conversationId만 전달하면 Advisor가 나머지 과정을 처리한다.


III. 가장 짧은 멀티턴 구현

먼저 MessageWindowChatMemory와 Advisor가 적용된 ChatClient를 구성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// infrastructure/ai/ChatMemoryConfig.java
@Configuration
public class ChatMemoryConfig {

    @Bean
    public ChatMemory chatMemory(ChatMemoryRepository repository) {
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(repository)
                .maxMessages(20)
                .build();
    }

    @Bean
    public ChatClient chatClient(
            ChatClient.Builder builder,
            ChatMemory chatMemory) {
        return builder
                .defaultAdvisors(
                        MessageChatMemoryAdvisor.builder(chatMemory).build()
                )
                .build();
    }
}

별도의 영속 저장소를 구성하지 않으면 Spring AI가 InMemoryChatMemoryRepository를 기본으로 제공한다. 이제 모델을 호출할 때 대화를 구분할 ID를 Advisor에 전달한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// infrastructure/ai/SpringAiConversationClient.java
@Component
@RequiredArgsConstructor
public class SpringAiConversationClient {
    private final ChatClient chatClient;

    public String chat(String conversationId, String message) {
        return chatClient.prompt()
                .user(message)
                .advisors(advisor -> advisor.param(
                        ChatMemory.CONVERSATION_ID,
                        conversationId
                ))
                .call()
                .content();
    }
}

같은 conversationId로 요청하면 앞선 대화가 다음 요청에 포함된다.

1
2
conversationClient.chat("room-42", "내 이름은 지크야");
conversationClient.chat("room-42", "내 이름이 뭐였지?");

Spring AI 1.1.6의 Memory Advisor는 ChatMemory.CONVERSATION_ID를 필수로 요구한다. 값을 전달하지 않으면 기본 ID를 사용하지 않고 IllegalArgumentException을 던진다.


IV. Advisor의 실행 흐름

위 코드가 실행될 때 MessageChatMemoryAdvisor는 다음 순서로 동작한다.

sequenceDiagram
    participant App as Application
    participant Advisor as MessageChatMemoryAdvisor
    participant Memory as ChatMemory
    participant Model as ChatModel

    App->>Advisor: message + conversationId
    Advisor->>Memory: get(conversationId)
    Memory-->>Advisor: 이전 메시지
    Advisor->>Memory: 현재 UserMessage 저장
    Advisor->>Model: 이전 메시지 + 현재 메시지
    Model-->>Advisor: AssistantMessage
    Advisor->>Memory: AssistantMessage 저장
    Advisor-->>App: 응답 반환

이전 대화는 System Prompt의 문자열로 합쳐지는 것이 아니라, 각각의 역할을 가진 Message 목록으로 모델에 전달된다. 덕분에 모델은 User와 Assistant의 발화를 구분할 수 있다.

과거에 사용되던 PromptChatMemoryAdvisor는 대화 내용을 System Prompt에 문자열로 합치는 방식이다. Spring AI 1.1.3부터 deprecated 되었으므로 1.1.6에서는 MessageChatMemoryAdvisor를 기본 선택으로 사용한다.


V. 운영 환경에서 확인할 부분

1. conversationId는 서버가 검증한다

conversationId는 어떤 대화 내용을 불러올지 결정하는 키다. 클라이언트가 전달한 값을 검증 없이 사용하면 다른 사용자의 대화가 섞이거나 노출될 수 있다.

하나의 사용자 ID를 그대로 대화 ID로 사용하는 것도 피하는 것이 좋다. 사용자와 채팅방의 소유 관계를 확인하고, 서버에서 발급한 대화 ID를 사용해야 한다.

2. ChatMemory는 전체 대화 기록이 아니다

MessageWindowChatMemory는 정해진 개수를 넘은 오래된 메시지를 제거한다. 따라서 ChatMemoryRepository를 영속 저장소로 교체해도 전체 대화가 계속 보존된다고 가정하면 안 된다.

1
2
ChatMemory  = 다음 모델 요청에 포함할 문맥
ChatHistory = 사용자에게 보여주거나 감사 목적으로 보관할 전체 기록

전체 대화 이력이 필요하다면 별도의 Conversation Message 저장 모델을 두는 편이 명확하다.

3. InMemory 저장소는 재시작하면 사라진다

InMemoryChatMemoryRepository는 개발과 테스트에는 편리하지만 애플리케이션을 재시작하면 내용이 사라진다. 운영 환경에서는 JDBC, Cassandra, Neo4j, MongoDB, Cosmos DB 같은 Repository 구현을 선택할 수 있다.

저장소를 바꾸더라도 ChatMemoryMessageChatMemoryAdvisor의 사용 방식은 동일하다. Repository Bean을 교체하면 기존 구성에 그대로 주입된다.

4. 메시지 개수와 토큰 사용량은 다르다

maxMessages(20)은 메시지 개수를 제한할 뿐 토큰 수를 보장하지 않는다. 짧은 메시지 20개와 긴 문서가 포함된 메시지 20개는 모델 비용과 Context Window 사용량이 크게 다르다.

Spring AI 1.1.6은 토큰 기준 Window 구현을 기본 제공하지 않는다. 대화가 길어지는 서비스라면 별도의 요약 정책이나 토큰 기준 ChatMemory 구현을 고려해야 한다.

5. Tool Calling 중간 메시지는 별도 고려가 필요하다

Spring AI 1.1.6에서는 Tool Calling 과정에서 모델과 주고받은 중간 메시지가 Chat Memory에 자동 저장되지 않는다. 기본 ChatModel 내부 Tool 실행에서는 중간 Tool Call과 Tool Response가 ChatClient의 Advisor 체인을 다시 통과하지 않기 때문이다.

Tool Call과 Tool Response까지 다음 턴의 문맥에 반드시 포함해야 한다면 내부 Tool 실행에 의존하지 않고, 실행 반복을 직접 제어하면서 메시지를 저장해야 한다.

Tool Calling의 기본 실행 구조는 Spring AI Tool Calling 설계: Tool과 DDD 계층의 책임 분리에서 별도로 정리했다.


VI. 결과

Spring AI의 멀티턴 대화는 다음 세 컴포넌트의 조합으로 구현할 수 있다.

1
2
3
ChatMemory               = 어떤 메시지를 문맥으로 유지할지 결정
ChatMemoryRepository     = 메시지를 저장하고 조회
MessageChatMemoryAdvisor = 모델 호출 전후에 메모리를 자동 적용

핵심은 모델이 이전 대화를 직접 기억하는 것이 아니다. 애플리케이션이 conversationId를 기준으로 과거 메시지를 찾고, Advisor가 이를 매 요청의 Prompt에 다시 포함한다.

간단한 기능이라면 기본 MessageWindowChatMemory만으로 충분하다. 운영 환경에서는 대화 ID의 소유권 검증, 영속 저장소, 전체 Chat History의 분리와 토큰 사용량까지 함께 고려해야 한다.


VII. 참고

  1. Spring AI 1.1.6 Release Notes
  2. Spring AI 1.1.6 - Chat Memory Reference
  3. Spring AI 1.1.6 - MessageChatMemoryAdvisor
  4. Spring AI 1.1.6 - MessageWindowChatMemory
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.