Spring에서 AI 붙이기: “일단 AI 뭐라도 넣어라”

“뭐라도 좋으니까 AI 좀 넣어봐”

AI 유행이 세상을 덮친 뒤로 개발자들이 자주 듣는 말이 있다.

“우리 제품에도 AI 좀 넣어야 하지 않겠어?”

“뭐 넣을까요?”

“그건 네가 알아서 해. 아무튼 AI 관련된 뭐라도 있어야 해.”

이메일 자동 분류든, 문서 요약이든, 사내 챗봇이든 어쨌든 ‘뭔가 AI스러운 것’을 붙여야 한다. 회사 Spring Boot 프로젝트를 열어보고, 개발자는 조용히 구글링을 시작한다. 가장 먼저 떠오르는 방법은 이거다.

“OpenAI API를 그냥 HTTP로 호출하면 되지 않나?”

맞다. 되긴 된다. 처음 한두 기능까진 된다. 그런데 세 번째 기능을 붙일 때쯤 이런 게 보인다. 프롬프트 템플릿 관리, 응답 JSON 파싱, 재시도와 타임아웃, 토큰 사용량 로깅, 모델별 차이 흡수, 스트리밍 처리, RAG용 벡터 DB 연결… 어느 순간 본 업무보다 AI 보일러플레이트가 더 커져 있다.

이 글에서는 ‘Spring AI’ 프레임워크를 중심으로, Spring 개발자가 AI 기능을 깔끔하게 붙이는 방법을 정리한다. 이전 글들에서 배운 개념들(프롬프트, 구조화된 출력, RAG, 도구 호출, 관측성)이 Spring 생태계에서는 어떤 모양으로 구현되는지 보는 글이다.

Raw API 호출도 돌아가지만, 프레임워크 없이 오래 유지하려면 결국 Spring AI 같은 도구를 직접 만들게 된다.

Spring AI가 뭔가

‘한 줄 요약: Spring이 JDBC, JPA, Security를 추상화한 것처럼, LLM 호출과 주변 기능을 추상화한 공식 프로젝트.’

Spring AI는 Spring 공식 AI 애플리케이션 프레임워크다. 2025년 5월 1.0 GA, 2025년 11월 1.1 GA에서 MCP, prompt caching, 모델 공급자, 관측성 기능이 크게 확장됐다. 2026년 4월 기준으로 1.1.x가 Spring Boot 3.5.x 계열의 안정 버전이고, 2.x는 Spring Boot 4.x를 향한 preview 라인이다. ‘공통 기능은 공급자 독립적으로, 고급 기능은 공급자별 차이를 감수하는 통합 계층’이 목표다.

미리 솔직히 말해두면 좋다. Spring AI는 ‘공통 기능은 잘 추상화되지만, reasoning/thinking, native structured output, prompt caching, MCP 지원 범위 같은 고급 기능은 공급자 차이가 그대로 드러난다’. 이 글에서도 장점과 한계를 함께 볼 것이다.

핵심 강점을 정리하면 이렇다.

영역제공하는 것
모델 공급자OpenAI, Anthropic, Google(Gemini), Azure OpenAI, Ollama, Bedrock 등 어댑터
대화 추상화ChatClient, ChatModel (Chat Completion 스타일)
구조화된 출력Java 클래스/레코드로 바로 매핑
RAG 지원VectorStore, DocumentReader, ETL 파이프라인
도구 호출@Tool 어노테이션, MCP client/server
관측성Micrometer 통합 (메트릭, 트레이스)
재시도/타임아웃Spring Retry 연동

한마디로, 이전 글들에서 다룬 개념 대부분에 대응되는 추상화를 Spring 스타일로 제공한다.


시작: 의존성 추가

‘한 줄 요약: BOM으로 버전을 묶고, 필요한 공급자 스타터만 추가하면 끝.’

// build.gradle (Spring Boot 3.5.x + Java 17+ 권장)
dependencies {
implementation platform("org.springframework.ai:spring-ai-bom:1.1.4")
implementation "org.springframework.ai:spring-ai-starter-model-openai"
// 또는 Anthropic, Gemini, Ollama 등 필요한 공급자
}

# application.yml
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o-mini  # 예시. 운영에서는 최신 가격/성능/지원 상태 확인
          temperature: 0.7

버전은 글을 읽는 시점의 Spring AI stable 버전에 맞춰 조정해야 한다. Spring AI 1.1.x는 Spring Boot 3.5.x 계열, 2.x는 Spring Boot 4.x를 타겟한다. 회사가 Boot 2.x + Java 8을 쓰고 있다면 업그레이드가 먼저다.


첫 호출: ChatClient

‘한 줄 요약: ChatClient 한 줄로 AI에게 말을 걸고, 응답 타입까지 지정할 수 있다.’

Spring AI의 가장 기본 진입점은 ChatClient다. HTTP 클라이언트처럼 빌더 패턴으로 쓴다.

@Service
public class AskService {

    private final ChatClient chat;

    public AskService(ChatClient.Builder builder) {
        this.chat = builder.build();
    }

    public String ask(String question) {
        return chat.prompt()
            .user(question)
            .call()
            .content();
    }
}

시스템 프롬프트(상위 지시)도 쉽게 붙일 수 있다.

public String ask(String question) {
    return chat.prompt()
        .system("당신은 Spring Boot 전문가입니다. 한국어로 답변합니다.")
        .user(question)
        .call()
        .content();
}

처음에는 .call().content() 스타일의 non-streaming으로 시작하는 편이 가장 단순하다. UI에서 토큰 단위로 결과를 흘려야 한다면 .stream().content()Flux<String>을 받을 수 있다. WebFlux와 잘 맞는다.

‘스트리밍 + tool calling + advisor를 섞을 때는 주의.’ Spring AI는 imperative/reactive 모델이 섞이는 지점이 있다. 이 조합을 쓰면 ‘호출 경계(어디까지가 reactive이고 어디서 blocking인가)와 관측성 span 연결이 제대로 이어지는지’를 꼭 확인하자. 기본 설정에서 span이 끊기거나 non-streaming 경로가 blocking으로 동작하는 경우가 있다.


구조화된 출력: Java 레코드로 바로 받기

‘한 줄 요약: AI 답변을 Java 클래스/레코드 타입으로 바로 매핑할 수 있다. 대부분의 경우 JSON Schema를 직접 다루지 않고 시작할 수 있다.’

이전 글(프롬프트와 컨텍스트 설계)에서 ‘정해진 양식으로 답받기(Structured Output)’ 이야기를 했다. Spring AI는 이걸 Java 타입으로 바로 연결해 준다.

public record ProjectSummary(
    String title,
    String priority,   // HIGH / NORMAL / LOW
    List<String> tags
) {}

public ProjectSummary summarize(String content) {
    return chat.prompt()
        .user("다음 프로젝트 설명을 요약해 주세요:\n" + content)
        .call()
        .entity(ProjectSummary.class);   // ← 핵심
}

Spring AI는 Java 타입을 바탕으로 형식 지시나 JSON Schema를 만들고, 모델 응답을 Java 객체로 변환해 준다. 컨트롤러에서 그대로 반환하면 그게 API 응답이 된다.

다만 주의할 점이 있다. 기본 StructuredOutputConverter 방식은 best effort이므로 ‘항상 스키마 준수가 보장되는 것은 아니다’. 공급자가 native structured output을 지원하는 경우에는 이를 활성화하면 더 안정적이다. 어느 쪽이든 ‘형식 안정성’은 크게 올라가지만, ‘값의 정확성’까지 보장되는 건 아니다. 운영 코드에서는 validation과 fallback을 함께 둬야 한다.


RAG: VectorStore로 연결

‘한 줄 요약: PGVector, Redis, Elasticsearch 등 다양한 벡터 저장소를 같은 인터페이스로 쓸 수 있다.’

이전 글(RAG 입문)에서 다룬 검색-주입 파이프라인을 Spring AI가 추상화해 준다. 핵심 인터페이스는 VectorStoreEmbeddingModel, 그리고 Advisor다.

벡터 저장소 설정

implementation 'org.springframework.ai:spring-ai-starter-vector-store-pgvector'

spring:
  ai:
    vectorstore:
      pgvector:
        schema-name: public
        table-name: vector_store
        dimensions: 1536  # 사용하는 EmbeddingModel의 출력 차원과 반드시 일치

dimensions는 벡터 DB 설정값이 아니라 ‘지금 쓰는 embedding model의 출력 차원’이다. 1536은 OpenAI text-embedding-3-small의 기본 차원 예시고, Gemini/Cohere/BGE/Bedrock Titan 등은 다르다. ‘직접 테이블 스키마를 만들었거나 고정 모델을 쓸 때만 명시하자.’ PgVectorStore는 EmbeddingModel에서 차원을 자동으로 가져올 수 있는데, 여기에 하드코딩한 값이 실제 모델 출력 차원과 다르면 색인이 통째로 깨진다. 모델을 바꾸면 기존 인덱스와 차원이 달라져 재색인이 필요할 수 있다는 점도 같은 이유에서다.

문서 적재 (ETL)

@Service
public class DocumentIngestionService {

    private final VectorStore vectorStore;

    public DocumentIngestionService(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    public void ingest(Resource pdfResource) {
        List<Document> docs = new PagePdfDocumentReader(pdfResource).read();
        List<Document> chunks = new TokenTextSplitter().apply(docs);
        vectorStore.add(chunks);
    }
}

데모 수준의 PDF라면 Reader → Splitter → VectorStore 흐름으로 빠르게 시작할 수 있다. 다만 운영 RAG에서는 PDF 파싱 품질, OCR, 표 처리, chunk 전략, metadata, 재색인 파이프라인이 품질을 크게 좌우한다(RAG 입문 글 참고).

질의: Advisor로 RAG 붙이기

Spring AI의 ‘Advisor’는 ChatClient 호출 전후에 끼어드는 미들웨어다. QuestionAnswerAdvisor를 붙이면 RAG 파이프라인이 완성된다.

@Service
public class RagService {

    private final ChatClient chat;

    public RagService(ChatClient.Builder builder, VectorStore vectorStore) {
        this.chat = builder
            .defaultAdvisors(
                QuestionAnswerAdvisor.builder(vectorStore)
                    .searchRequest(SearchRequest.builder()
                        .similarityThreshold(0.8)
                        .topK(6)
                        .build())
                    .build()
            )
            .build();
    }

    public String ask(String question) {
        return chat.prompt()
            .user(question)
            .call()
            .content();
    }
}

Advisor가 질문을 받아서 VectorStore에서 관련 문서를 검색하고, 검색 결과를 프롬프트에 자동으로 주입한다. 더 복잡한 RAG는 RetrievalAugmentationAdvisor 같은 모듈형 구성을 고려할 수 있다.

‘사내 RAG에서는 권한도 함께 검색되어야 한다.’ tenantId, department, securityLevel, documentStatus 같은 메타데이터 필터는 필수다. 사용자가 볼 수 없는 문서는 VectorStore 검색 결과에도 나오면 안 된다. Spring AI의 SearchRequest filter expression을 이용하면 Advisor 단위 또는 요청 단위로 검색 범위를 제한할 수 있다.


도구 호출: @Tool 어노테이션

‘한 줄 요약: 자바 메서드에 @Tool만 붙이면 AI가 호출할 수 있는 도구가 된다.’

이전 글(외부 세계 연결)에서 다룬 Function Calling을 Spring AI는 이렇게 추상화한다.

@Service
public class WeatherTools {

    @Tool(description = "특정 도시의 현재 날씨를 조회한다")
    public String getWeather(
        @ToolParam(description = "도시 이름") String city
    ) {
        // 실제 날씨 API 호출
        return weatherApi.fetch(city);
    }
}

이 도구를 AI에게 넘기려면 ChatClient 호출에 포함시킨다.

public String ask(String question, WeatherTools tools) {
    return chat.prompt()
        .user(question)
        .tools(tools)
        .call()
        .content();
}

사용자가 “오늘 서울 날씨 어때?”라고 물으면, Spring AI가 @Tool 메서드의 시그니처와 설명을 모델에 전달한다. 모델이 getWeather("서울") 호출을 요청하면, Spring AI가 실제 메서드를 실행하고 결과를 다시 모델에 돌려줘서 최종 답변을 완성한다. ‘tool schema 전달 → 모델의 tool call 요청 → 실제 메서드 실행 → 결과 재주입’ 루프를 프레임워크가 감싸주는 구조다.

‘@Tool은 편하지만 위험하다.’ 모델에게 “호출 가능한 메서드 목록”을 주는 것이므로, 읽기 도구와 쓰기 도구를 같은 수준으로 노출하면 안 된다. 조회 도구는 자동 실행하더라도, 이메일 발송·결제·DB 수정·파일 삭제 같은 도구는 별도 승인 흐름을 둬야 한다. defaultTools()에 공통 도구를 등록하면 편하지만 모든 요청에서 호출 가능해지므로, 사용자 권한/업무 흐름에 따라 요청 단위의 .tools(...)로 필요한 도구만 넘기는 설계가 더 안전하다.

MCP: Spring AI 1.1에서 크게 확장된 영역

MCP 연결도 Spring AI가 지원한다. spring-ai-starter-mcp-client 스타터로 외부 MCP 서버의 도구를 끌어와 ChatClient에 붙이고, 반대로 spring-ai-starter-mcp-server로 내 앱을 MCP 서버로 노출할 수도 있다. Spring AI 1.1은 STDIO/SSE/Streamable HTTP transport, @McpTool·@McpResource·@McpPrompt 같은 annotation 모델을 제공한다.

다만 운영 환경의 remote MCP는 ‘단순 연결’이 아니라 ‘설계’가 필요하다. 인증, 권한, tool allowlist, timeout, 감사 로그, 민감정보 마스킹을 함께 잡아야 한다. 특히 MCP Security 쪽은 빠르게 발전 중인 영역이라 버전별 문서 확인이 필수다. token passthrough(외부 서비스 토큰을 MCP 서버에 그대로 넘기는 방식)는 MCP 보안 문서에서 명시적으로 금지되는 anti-pattern이다.


관측성: Micrometer로 자동

‘한 줄 요약: 호출 시간·토큰 사용량 같은 기본 메트릭은 자동으로 수집된다. 단, 프롬프트/응답 본문은 민감정보 때문에 기본적으로 로깅되지 않는다.’

이전 글(평가와 하네스)에서 강조한 ‘실행 기록(Traces)’과 ‘비용/성능 추적’이 Spring AI에서는 Actuator/Micrometer를 통해 연결된다. Spring 개발자에게 익숙한 방식이다.

management:
  endpoints:
    web:
      exposure:
        include: prometheus, metrics
  metrics:
    tags:
      application: ${spring.application.name}

ChatClient, ChatModel, Advisor, VectorStore 등의 호출 시간과 토큰 사용량이 OpenTelemetry/Micrometer semantic conventions 기반 메트릭과 트레이스로 노출된다. 기존 APM 인프라(Prometheus/Grafana, Tempo, Jaeger, Datadog 등)에 그대로 들어간다.

‘주의: 프롬프트와 응답 본문은 기본적으로 export되지 않는다.’ 개인정보와 사내 데이터가 섞일 수 있기 때문에 Spring AI가 의도적으로 끈 설정이다. 디버깅을 위해 본문 로깅을 켤 때는 ‘마스킹, 접근 권한, 보존 기간, 샘플링 정책’을 함께 설계해야 한다(평가와 하네스 글 참고).


비용: Spring AI가 해결해주지 않는다

‘한 줄 요약: 프레임워크가 편해질수록 호출·토큰은 늘어나기 쉽다. Spring AI는 비용을 “보이게” 해주지, “줄여주지” 않는다.’

Spring AI는 LLM 호출을 단순화해준다. 그런데 이 편함이 함정이 될 수 있다. RAG, tool calling, retry, MCP를 쉽게 붙일 수 있다는 건, 잘못 설계하면 한 요청 안에서 호출 수와 토큰이 기하급수적으로 늘어난다는 뜻이기도 하다.

비용이 늘어나는 주요 지점:

지점설명방어책
LLM 호출모든 ChatClient 호출 = API 비용모델 라우팅, max tokens, 요청당 예산 상한
Retry실패/timeout 시 재시도로 호출 수 증가재시도 횟수 조정, idempotency 설계
RAGembedding, 검색, 긴 context 주입topK 제한, chunk 튜닝, 결과 캐시
Tool calling한 요청 안에서 LLM↔도구 루프 반복 가능tool allowlist, 반복 횟수 제한
Observabilityprompt/응답 저장 시 저장 비용 + 보안 리스크샘플링, 마스킹, 보존 기간

Spring AI의 retry 설정은 spring.ai.retry.* 공통 prefix로 모든 공급자에 걸쳐 적용된다(OpenAI 전용이 아니다). 기본값은 max attempts 10, initial interval 2초, multiplier 5, max interval 3분이다. 안정성에는 도움이 되지만, timeout 뒤에도 서버 쪽에서 이미 처리된 요청이 재시도되면 비용·중복 실행 문제가 생긴다. OpenAI의 n(생성 후보 수) 옵션도 토큰 비용에 직접 영향을 미치므로 기본 1로 유지하는 편이 안전하다.

반대로 Spring AI가 비용을 ‘줄이기 쉽게’ 만드는 기능도 있다. Spring AI 1.1은 Anthropic Claude와 AWS Bedrock의 ‘prompt caching’을 지원한다. 반복되는 시스템 프롬프트나 긴 tool definition이 있는 에이전트·RAG 앱에서는 비용을 크게 줄일 수 있다. 운영에서는 ‘요청당 max token, 요청당 예산, retry 제한, tool 호출 횟수 제한, RAG topK 제한’을 반드시 둬야 한다.


실무 팁

1. 공급자 교체 비용을 낮추자 (단, “무수정 교체”는 아님)

spring-ai-starter-model-openai를 썼다고 해서 코드가 OpenAI에 묶이진 않는다. 같은 ChatClient 인터페이스로 Anthropic, Gemini, Ollama로 공급자를 바꿀 수 있다. 초기 개발은 저렴한 모델, 프로덕션은 고성능 모델, 로컬 테스트는 Ollama로 가는 전략이 쉽다.

다만 ‘Spring AI가 공급자 차이를 완전히 없애주는 것은 아니다.’ 단순 chat, embedding, 기본 tool calling은 비교적 잘 추상화된다. 그러나 ‘reasoning/thinking 옵션, native structured output, prompt caching, citation, grounding, MCP 지원 범위, 안전 정책’은 공급자마다 다르다. 공통 API로 시작하되, 운영에서는 공급자별 옵션을 감싸는 얇은 adapter 계층을 두는 편이 안전하다.

2. 비용/속도 라우팅은 일찍 고민하자

Spring AI의 ChatClient.Builder는 여러 모델을 프로필/용도별로 나눠 등록할 수 있다. 단순 분류는 작은 모델, 복잡한 판단은 큰 모델로 라우팅하는 구조를 처음부터 잡아두면 비용 관리가 쉽다.

3. 프롬프트는 리소스로 분리

인라인 문자열로 프롬프트를 박지 말고, src/main/resources/prompts/*.st 파일로 분리한다. Spring AI의 PromptTemplate이 StringTemplate 스타일로 변수 바인딩을 해준다. 프롬프트 변경이 코드 변경이 아닌 리소스 변경이 되면, A/B 테스트나 배포 분리가 쉬워진다.

4. 엔터프라이즈 환경은 Azure/Bedrock/Google GenAI

공용 OpenAI API가 금지된 환경이라면 Azure OpenAI, AWS Bedrock, Google GenAI/Vertex AI 모드 같은 엔터프라이즈 경로를 검토할 수 있다. 다만 클라우드별 어댑터와 모델 통합은 버전별 변화가 크다. Spring AI 2.0.0-M4에서는 기존 Vertex AI 클래스가 deprecated되고 Google GenAI로 통합되는 등의 이동이 있었다. Spring AI와 클라우드 공급자의 최신 문서를 함께 확인해야 한다.

5. @Tool 남발 주의

편하다고 @Tool을 많이 붙이면, AI에게 주어지는 도구 설명이 길어져 토큰과 판단 혼란이 늘어난다. 정말 필요한 도구만 노출하고, 사용자별로 허용 도구를 제한하는 설계를 권한다.


Spring AI가 잘 안 맞는 경우

Spring AI가 항상 정답은 아니다. 아래 경우에는 다른 선택이 더 자연스럽다.

상황더 나은 선택
OpenAI API를 한 번만 호출하는 작은 스크립트raw SDK (OpenAI Python/Node SDK 등)
Python 기반 데이터/ML 파이프라인과 강하게 엮인 팀LangChain, LlamaIndex, 공급자 SDK
공급자의 최신 기능을 출시 직후 바로 써야 함공급자 SDK 직접 사용 (Spring AI 추상화가 따라올 때까지)
Spring Boot 2.x / Java 8 레거시업그레이드 먼저. 또는 raw HTTP로 구현

Spring AI는 ‘Spring Boot 서비스 안에 AI 기능을 안정적으로 넣고 운영해야 하는 팀’에게 가장 잘 맞는다. 기존 운영 스택이 Actuator/Micrometer/Prometheus/OpenTelemetry/Spring Security/Spring Data 중심이면, raw HTTP로 다 짜는 것보다 Spring AI로 시작하는 편이 합리적이다.


전체 그림: AI Decoded 시리즈의 개념이 Spring AI에서는 어디에

이전 글들에서 다룬 개념이 Spring AI의 어느 부분에 해당하는지 정리하면 이렇다.

개념Spring AI 대응
프롬프트/시스템 메시지ChatClient.prompt().system()/.user()
구조화된 출력.entity(MyRecord.class)
도구 호출@Tool 어노테이션
MCP spring-ai-starter-mcp-client / -server
RAGVectorStore + QuestionAnswerAdvisor
에이전트 루프 Advisor 체인, Tool 호출 반복
평가/관측 Micrometer metrics, OpenTelemetry traces

Spring 생태계에 이미 익숙하다면, ‘이미 아는 방식으로 AI를 쓸 수 있게 해 주는 도구’라고 이해해도 괜찮다.


마치며: AI도 결국 “그 프레임워크”로 들어온다

정리하면:

  • Spring AI는 Spring 공식의 AI 통합 계층이다. 모델 공급자, 대화, 구조화된 출력, RAG, 도구 호출, MCP, 관측성을 일관된 추상화로 제공한다.
  • 진입점은 ChatClient. 빌더 패턴으로 호출하고, .entity(Class)로 타입 안전한 응답을 받을 수 있다(항상 보장은 아니므로 검증 필요).
  • RAG는 VectorStore + Advisor로 붙이고, 도구 호출은 @Tool 어노테이션으로 붙인다. 사내 RAG는 메타데이터 필터로 권한까지 검색에 반영해야 한다.
  • Micrometer/OpenTelemetry와 연동되어 기본 메트릭·트레이스는 자동. 단, 프롬프트/응답 본문은 보안상 기본 비노출.
  • 공통 기능은 공급자 교체 비용이 낮지만, reasoning/캐싱/structured output/MCP 같은 고급 기능은 공급자마다 다르다. ‘무수정 교체’는 기대하지 말자.
  • Spring AI는 비용을 없애주지 않는다. 요청당 max token, retry 제한, tool 루프 제한, RAG topK 제한을 운영 기본값으로 두자.

Spring이 과거에 JDBC, JPA, Security, Boot를 거치며 해온 일을 AI에도 똑같이 하고 있는 셈이다. 처음부터 raw HTTP로 다 짜는 것보다, Spring AI로 시작해서 필요할 때만 직접 구현으로 내려가는 편이 현실적이다.

다음 글에서는 AI를 공용 API가 아니라 ‘내 컴퓨터 안’에서 돌리는 방법을 다룬다. Ollama, vLLM 같은 도구로 로컬 LLM을 운영할 때 고려해야 할 것들, 양자화와 GPU/비용 이야기다. 사내 데이터를 외부로 보내면 안 되는 환경이나, 요금 부담 없이 실험하고 싶은 개인 개발자에게 유용한 주제다.

댓글 남기기