Spring AI

김영준·2026년 8월 29일

spring

목록 보기
1/1

Spring AI, 왜 등장했나

기존 Spring 환경에서 LLM/AI 기능을 추가하려면 Python 기반 환경을 따로 띄워야 했음. 싸피 프로젝트에서도 이렇게 AI를 직접 구현하고(내용은 잘 기억 안 남) 연동해서 띄웠던 걸로 기억함.

근데 이제는 이미 만들어진 AI를 불러다 쓸 수 있음. 또한 직접 모든 코드를 짜서 부르는 게 아니라, 어느 정도 그 과정 자체가 간편화됨. 그게 바로 Spring AI의 등장.

  • Python 서버 필요 없음
  • LangChain 별도 구현 불필요
  • 프롬프트 / LLM 호출 / 응답 처리 모두 Spring 내부에서 해결 가능

또 모델이 굉장히 많은데, 과거에는 각 모델의 API를 직접 호출해 기능을 만들어야 했음. 모델마다 엔드포인트가 다르고 여러 API를 쓰는 게 불편했음. ChatGPT / Claude / Gemini 등 각각 다르게 써야 했는데, Spring AI의 등장으로 하나의 규격으로 쓰는 게 편리해짐.

즉 이것도 나름 전략 패턴이라고 할 수 있지 않을까? ㅇㅇ


실제 코드 (EmbeddingService)

// src/main/java/.../post/service/search/EmbeddingService.java
private final EmbeddingModel embeddingModel;   // Spring AI 인터페이스

public float[] embed(String text) { // 캐시활용
    return queryEmbeddingCache.get(text, text2 -> embeddingModel.embed(text2));
}

public float[] embedWithoutCache(String text) { // 노캐시
    return embeddingModel.embed(text);
}

@Retryable(retryFor = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
@Transactional
public void generateAndSave(Long postId, String text) { // 임베딩한 데이터 DB에 저장
    generationTimer.record(() -> {
        float[] embedding = embedWithoutCache(text);
        therapyPostRepository.updateContentEmbedding(postId, toVectorString(embedding));
    });
    successCounter.increment();
}

EmbeddingModel은 스프링이 AI를 자동 구성으로 만들어주는 빈. 직접 new하지 않고 생성자 주입을 받으면 됨.
그리고 만약 똑같은 검색을 자주 한다면 비용을 절약하기 위해 카페인캐시를 두었음.

설정은 이렇게:

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY:}
      embedding:
        options:
          model: text-embedding-3-small
    model:
      chat: none        # OpenAI의 다른 기능(챗, 이미지 등)은 전부 꺼둠
      embedding: none    # 기본은 꺼두고, 검색 기능 프로필에서만 켜는 구조
      image: none
      moderation: none

Spring AI가 없다면?

Spring AI가 대신해주는 부분을 보여주기 위한 비교용 코드. (실제 저장소엔 없음, 가상 예시)

@Component
public class OpenAiEmbeddingClient {
    private final RestClient restClient;   // RestClient 빌드
    private final ObjectMapper objectMapper;
    private final String apiKey;
    private final String model;

    public OpenAiEmbeddingClient(@Value("${OPENAI_API_KEY}") String apiKey,
                                  @Value("${openai.embedding.model:text-embedding-3-small}") String model) {
        this.apiKey = apiKey;
        this.model = model;
        this.objectMapper = new ObjectMapper();
        Duration timeout = Duration.ofSeconds(10);           // 타임아웃 설정
        this.restClient = RestClient.builder()
                .baseUrl("https://api.openai.com/v1")
                .requestFactory(ClientHttpRequestFactories.get(
                        ClientHttpRequestFactorySettings.DEFAULTS
                                .withConnectTimeout(timeout)
                                .withReadTimeout(timeout)))
                .build();
    }

    public float[] embed(String text) {
        Map<String, Object> body = Map.of(          // JSON 요청 바디 구성
                "model", model,
                "input", text
        );
        String response = restClient.post()
                .uri("/embeddings")
                .header("Authorization", "Bearer " + apiKey)
                .contentType(MediaType.APPLICATION_JSON)
                .body(body)
                .retrieve()
                .body(String.class);
        return parseEmbedding(response);             // 응답 파싱
    }

    private float[] parseEmbedding(String response) {
        try {
            JsonNode root = objectMapper.readTree(response);
            JsonNode values = root.path("data").get(0).path("embedding");
            float[] embedding = new float[values.size()];
            for (int i = 0; i < values.size(); i++) {
                embedding[i] = (float) values.get(i).asDouble();
            }
            return embedding;
        } catch (Exception e) {
            throw new RuntimeException("OpenAI embedding response parsing failed", e);
        }
    }
}

위의 코드가 아래 한 줄로 대체됨:

public float[] embedWithoutCache(String text) {
    return embeddingModel.embed(text);
}
항목직접 구현 시Spring AI 사용 시
HTTP 클라이언트 생성/타임아웃 설정RestClient.builder()... 직접 작성자동 구성이 내부적으로 처리
인증 헤더 붙이기"Authorization", "Bearer " + apiKey 직접 작성spring.ai.openai.api-key 설정만 하면 끝
요청 바디 JSON 스키마OpenAI 스펙(model, input 필드명 등)을 코드로 숙지·작성embeddingModel.embed(text) 한 줄
응답 파싱data[0].embedding 경로를 알고 JsonNode로 직접 파싱float[]를 그냥 리턴받음
에러 처리(4xx/5xx, 재시도 대상 판단)직접 예외 매핑라이브러리가 표준화된 예외 제공

내 서버에서 켜려면

application.yaml을 수정하거나:

spring.ai.model.embedding: none -> openai
app.search.embedding.enabled: false -> true
app.search.strategy: gin -> vector

또는 deploy.yml을 변경하면 됨:

"aws secretsmanager get-secret-value --secret-id community/melonne/openai/staging --region ap-northeast-2 --query SecretString --output text >> /tmp/staging.env",
"echo SPRING_AI_MODEL_EMBEDDING=openai >> /tmp/staging.env",
"echo APP_SEARCH_EMBEDDING_ENABLED=true >> /tmp/staging.env",
"echo APP_SEARCH_STRATEGY=vector >> /tmp/staging.env",

근데 application.yaml에서 비밀 아닌 값(embedding on/off, strategy 등)은 그냥 처리하고, OpenAI API 키 어디서 가져올지만 deploy.yml에 명시하면 될 듯.

profile
개발의 신이 될거다

0개의 댓글