
LLM을 서비스에 붙여보면 금방 깨닫게 됩니다. 사용자는 비슷한 질문을 계속 던지고, 애플리케이션은 그때마다 모델을 다시 호출합니다. 결국 비슷한 답변을 얻기 위해 매번 비용을 지불하게 되고, 응답 시간도 느려집니다.
이 글은 이런 고민을 가진 개발자를 위해 준비했습니다. Spring AI와 Redis Vector Store를 활용해 이미 답한 질문에 대한 LLM 호출을 건너뛸 수 있는 시맨틱 캐싱을 직접 구현하는 방법을 정리합니다. 설정부터 코드 작성, 테스트까지 흐름대로 설명하므로 그대로 따라 하면 바로 적용할 수 있습니다.
시맨틱 캐싱(Semantic Caching)이란 무엇인가
시맨틱 캐싱은 단순히 동일한 문자열을 비교하는 캐싱이 아니라, 질문의 의미(semantic meaning)를 비교해 비슷한 질문에 대해 기존 답변을 재사용하는 방식입니다.
흐름은 간단합니다.
- 사용자의 질문을 임베딩 모델로 벡터화한다.
- Redis 같은 벡터 스토어에 질문 벡터와 답변을 저장한다.
- 새로운 질문이 들어오면 먼저 벡터 스토어에서 의미적으로 유사한 질문을 검색한다.
- 유사도가 기준치 이상이면 기존 답변을 반환하고, 아니면 새로운 LLM 호출을 수행한다.
이 방식의 장점은 명확합니다.
비슷한 질문이 반복될수록 비용이 줄고, 응답 속도도 빨라집니다.
프로젝트 구성: Spring AI + OpenAI Embedding + Redis Vector Store
시맨틱 캐싱의 핵심은 크게 두 가지입니다.
- 텍스트를 벡터로 변환하는 임베딩 모델
- 벡터를 저장하고 유사도를 검색하는 벡터 스토어
Spring AI는 이 두 가지를 모두 쉽게 구성할 수 있도록 지원합니다.
OpenAI 임베딩 모델 설정
먼저 OpenAI 임베딩 모델을 사용할 수 있도록 의존성을 추가합니다.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.3</version>
</dependency>
그리고 application.yaml에서 API 키와 embedding 모델을 설정합니다.
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
embedding:
options:
model: text-embedding-3-small
dimensions: 512
이 설정으로 Spring AI는 EmbeddingModel 빈을 자동 생성합니다.
Redis Vector Store 설정
Redis를 벡터 스토어로 사용하기 위해 다음 의존성을 추가합니다.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
<version>1.0.3</version>
</dependency>
그리고 Redis 연결 정보를 설정합니다.
spring:
data:
redis:
url: ${REDIS_URL}
이후 시맨틱 캐시에 필요한 속성을 설정합니다.
com:
baeldung:
semantic:
cache:
similarity-threshold: 0.8
content-field: question
embedding-field: embedding
metadata-field: answer
threshold 값은 얼마나 비슷해야 캐시에서 답변을 반환할지 결정하는 기준입니다.
이제 RedisVectorStore를 초기화하는 설정 클래스를 만듭니다.
@Configuration
@EnableConfigurationProperties(SemanticCacheProperties.class)
class LLMConfiguration {
@Bean
JedisPooled jedisPooled(RedisProperties redisProperties) {
return new JedisPooled(redisProperties.getUrl());
}
@Bean
RedisVectorStore vectorStore(
JedisPooled jedisPooled,
EmbeddingModel embeddingModel,
SemanticCacheProperties semanticCacheProperties
) {
return RedisVectorStore
.builder(jedisPooled, embeddingModel)
.contentFieldName(semanticCacheProperties.contentField())
.embeddingFieldName(semanticCacheProperties.embeddingField())
.metadataFields(
RedisVectorStore.MetadataField.text(semanticCacheProperties.metadataField()))
.build();
}
}
이제 RedisVectorStore를 사용해 문서 저장과 검색을 처리할 수 있습니다.
시맨틱 캐싱 서비스 구현하기
다음 단계는 캐싱 로직을 실제 서비스 코드로 구현하는 것입니다.
LLM 응답을 캐시에 저장하기
아래는 질문과 답변을 Redis Vector Store에 저장하는 코드입니다.
@Service
@EnableConfigurationProperties(SemanticCacheProperties.class)
class SemanticCachingService {
private final VectorStore vectorStore;
private final SemanticCacheProperties semanticCacheProperties;
void save(String question, String answer) {
Document document = Document
.builder()
.text(question)
.metadata(semanticCacheProperties.metadataField(), answer)
.build();
vectorStore.add(List.of(document));
}
}
문서를 저장하면 Spring AI가 내부적으로 질문 텍스트를 embedding 모델로 변환해 Redis에 저장합니다.
캐시에서 의미적으로 유사한 결과 검색하기
사용자가 새로운 질문을 입력하면 다음 코드가 실행됩니다.
Optional<String> search(String question) {
SearchRequest searchRequest = SearchRequest.builder()
.query(question)
.similarityThreshold(semanticCacheProperties.similarityThreshold())
.topK(1)
.build();
List<Document> results = vectorStore.similaritySearch(searchRequest);
if (results.isEmpty()) {
return Optional.empty();
}
Document result = results.getFirst();
return Optional
.ofNullable(result.getMetadata().get(semanticCacheProperties.metadataField()))
.map(String::valueOf);
}
topK(1)은 가장 유사한 문서 하나만 가져오겠다는 의미입니다.
threshold 이상이면 캐시 히트로 간주하고 답변을 반환합니다.
테스트로 확인하는 시맨틱 캐싱 동작
다음 테스트 코드를 보면 동작 과정이 명확합니다.
String question = "How many sick leaves can I take?";
String answer = "No leaves allowed! Get back to work!!";
semanticCachingService.save(question, answer);
String rephrasedQuestion = "How many days sick leave can I take?";
assertThat(semanticCachingService.search(rephrasedQuestion))
.isPresent()
.hasValue(answer);
String unrelatedQuestion = "Can I get a raise?";
assertThat(semanticCachingService.search(unrelatedQuestion))
.isEmpty();
- 비슷한 질문: 기존 답변 반환
- 완전히 다른 질문: 캐시 미스
실제 서비스에서도 이런 방식으로 유사 질문에 대한 중복 LLM 호출을 줄일 수 있습니다.
Spring AI 기반 시맨틱 캐싱이 주는 실질적 가치
이 글에서 살펴본 시맨틱 캐싱 구현 방법은 비교적 단순하지만 효과는 분명합니다.
LLM 호출 비용을 줄이고, 응답 속도를 높이며, 사용자 경험 개선에도 기여합니다.
정리하면 다음과 같습니다.
- 임베딩 모델을 사용해 질문을 벡터로 변환한다.
- Redis Vector Store로 의미 기반 검색을 수행한다.
- 유사한 질문에 대한 답변은 LLM을 호출하지 않고 즉시 반환한다.
시맨틱 캐싱은 특히 고객센터 챗봇, 검색 기반 Q&A 서비스, 문서 질의 응답 시스템 등 반복적 질문이 많은 환경에서 큰 비용 절감 효과를 제공합니다.
Spring AI의 자동 설정과 Redis Vector Store를 활용하면 복잡해 보이는 시맨틱 검색 로직을 손쉽게 구현할 수 있습니다.
향후에는 이 구조를 RAG 기반 검색 시스템이나 고도화된 챗봇에 확장해 적용할 수도 있습니다.
이제 여러분의 Spring 애플리케이션에도 시맨틱 캐싱을 적용해 효율적인 LLM 활용 환경을 구축해 보시기 바랍니다.

'Spring' 카테고리의 다른 글
| Spring Boot 4.0 출시: 개발자가 꼭 알아야 할 변화와 실무 활용 포인트 (0) | 2025.12.02 |
|---|---|
| Beyond JSON: Spring AI에서 TOON·XML·CSV·YAML로 툴 응답 포맷을 전환하는 방법 (0) | 2025.12.01 |
| AI가 AI를 평가하고 스스로 개선한다: Spring AI Recursive Advisors를 활용한 LLM-as-a-Judge 구축 (0) | 2025.11.16 |
| Spring Batch 6.0 마이그레이션 가이드: 꼭 알아야 할 변경 사항 (0) | 2025.08.20 |
| 스프링 vs 스프링 부트 차이, 그리고 꼭 알아야 할 스프링 부트 핵심 개념 3가지 (0) | 2025.07.02 |