본문 바로가기

Spring

Spring AI와 Redis로 구현하는 시맨틱 캐싱: LLM 비용을 줄이는 가장 실용적인 방법

728x90
반응형
728x170

LLM을 서비스에 붙여보면 금방 깨닫게 됩니다. 사용자는 비슷한 질문을 계속 던지고, 애플리케이션은 그때마다 모델을 다시 호출합니다. 결국 비슷한 답변을 얻기 위해 매번 비용을 지불하게 되고, 응답 시간도 느려집니다.
이 글은 이런 고민을 가진 개발자를 위해 준비했습니다. Spring AI와 Redis Vector Store를 활용해 이미 답한 질문에 대한 LLM 호출을 건너뛸 수 있는 시맨틱 캐싱을 직접 구현하는 방법을 정리합니다. 설정부터 코드 작성, 테스트까지 흐름대로 설명하므로 그대로 따라 하면 바로 적용할 수 있습니다.

반응형

시맨틱 캐싱(Semantic Caching)이란 무엇인가

시맨틱 캐싱은 단순히 동일한 문자열을 비교하는 캐싱이 아니라, 질문의 의미(semantic meaning)를 비교해 비슷한 질문에 대해 기존 답변을 재사용하는 방식입니다.

흐름은 간단합니다.

  1. 사용자의 질문을 임베딩 모델로 벡터화한다.
  2. Redis 같은 벡터 스토어에 질문 벡터와 답변을 저장한다.
  3. 새로운 질문이 들어오면 먼저 벡터 스토어에서 의미적으로 유사한 질문을 검색한다.
  4. 유사도가 기준치 이상이면 기존 답변을 반환하고, 아니면 새로운 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 호출을 줄일 수 있습니다.


728x90

Spring AI 기반 시맨틱 캐싱이 주는 실질적 가치

이 글에서 살펴본 시맨틱 캐싱 구현 방법은 비교적 단순하지만 효과는 분명합니다.
LLM 호출 비용을 줄이고, 응답 속도를 높이며, 사용자 경험 개선에도 기여합니다.

정리하면 다음과 같습니다.

  • 임베딩 모델을 사용해 질문을 벡터로 변환한다.
  • Redis Vector Store로 의미 기반 검색을 수행한다.
  • 유사한 질문에 대한 답변은 LLM을 호출하지 않고 즉시 반환한다.

시맨틱 캐싱은 특히 고객센터 챗봇, 검색 기반 Q&A 서비스, 문서 질의 응답 시스템 등 반복적 질문이 많은 환경에서 큰 비용 절감 효과를 제공합니다.
Spring AI의 자동 설정과 Redis Vector Store를 활용하면 복잡해 보이는 시맨틱 검색 로직을 손쉽게 구현할 수 있습니다.

향후에는 이 구조를 RAG 기반 검색 시스템이나 고도화된 챗봇에 확장해 적용할 수도 있습니다.

이제 여러분의 Spring 애플리케이션에도 시맨틱 캐싱을 적용해 효율적인 LLM 활용 환경을 구축해 보시기 바랍니다.

300x250

https://www.baeldung.com/spring-ai-semantic-caching?fbclid=IwY2xjawOOfqVleHRuA2FlbQIxMQBzcnRjBmFwcF9pZBAyMjIwMzkxNzg4MjAwODkyAAEePwU00Yt-IIpDu9FSYXd3i8VHl3WihQ5DrRLSqMV11LzNCJU8rSSjbwHLtBs_aem_jdATqe64RjBp9leRIGEdUg

 

728x90
반응형
그리드형