Spring AI

소개와 모델 세팅

Spring AI는 LLM(거대 언어 모델) 같은 AI 모델을 스프링 애플리케이션에 자연스럽게 끼워 넣게 해 주는 프레임워크다. OpenAI·Anthropic·Google 등 제각각인 AI API를, 우리가 이미 아는 스프링 방식(빈 주입·자동설정)으로 통일해 다룬다. 분량이 많아 여덟 편으로 나누어, 하나의 실습 프로젝트를 키워 가며 다룬다.

이 장의 구성

  1. 소개와 모델 세팅 ← 현재 편 (Spring AI 개념·의존성·API 키·모델 2개 설정)

  2. 기본 채팅과 모델 비교 (ChatClient·ChatModel·스트리밍)

  3. 프롬프트 엔지니어링 (PromptTemplate·Zero/Few-shot·역할 부여)

  4. 구조화 출력 (응답을 Java 객체로 매핑)

  5. Tool Calling (LLM이 우리 메서드를 호출)

  6. RAG (우리 데이터로 답하게 하기)

  7. 멀티모달 - 이미지 (이미지 입력·생성)

  8. 멀티모달 - 음성 (STT·TTS)


1. Spring AI란

LLM을 직접 쓰려면 각 회사의 REST API에 HTTP 요청을 만들고, JSON을 직렬화·역직렬화하고, 인증·재시도·스트리밍을 일일이 처리해야 한다. Spring AI는 이 반복 작업을 모델 추상화로 감싸, 핵심 코드만 남긴다.

flowchart LR
    App["우리 애플리케이션<br/>(@Service)"] --> SAI["Spring AI<br/>ChatClient / ChatModel"]
    SAI --> P1["OpenAI"]
    SAI --> P2["Anthropic"]
    SAI --> P3["Google · Ollama · ..."]

개념

설명

ChatModel

채팅 모델을 호출하는 저수준 API. Prompt를 넣고 ChatResponse를 받는다

ChatClient

위를 감싼 고수준 플루언트 API. .prompt().user(...).call()처럼 읽기 쉽게 호출

EmbeddingModel

텍스트를 벡터로 바꾸는 모델 (RAG에서 사용)

자동설정(Auto-config)

API 키만 설정하면 위 빈들을 스프링이 자동으로 만들어 준다

왜 추상화가 중요한가

같은 코드로 모델만 바꿔 끼울 수 있다. OpenAI로 개발하다가 비용·성능 때문에 다른 모델로 갈아탈 때, 비즈니스 로직은 그대로 두고 설정만 바꾸면 된다. 이 장에서 모델 2개를 등록해 바꿔 쓰는 것도 같은 원리다.


2. 프로젝트 의존성

Spring Boot 프로젝트에 Spring AI BOM과 OpenAI 스타터를 추가한다. (Gradle 기준, 이 강의 실습 프로젝트의 build.gradle)

ext {
    set('springAiVersion', "1.1.7")   // Spring AI 버전
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'   // 스트리밍(SSE)용

    // OpenAI 모델(채팅·임베딩·이미지·음성)을 한 번에 제공하는 스타터
    implementation 'org.springframework.ai:spring-ai-starter-model-openai'

    // RAG에서 쓸 문서 리더와 RAG 어드바이저
    implementation 'org.springframework.ai:spring-ai-pdf-document-reader'
    implementation 'org.springframework.ai:spring-ai-advisors-vector-store'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
    }
}

스타터 하나로 끝

spring-ai-starter-model-openai 하나가 채팅·임베딩·이미지 생성·음성(STT/TTS) 모델을 모두 자동설정한다. 각 모델 빈(OpenAiChatModel, OpenAiImageModel, …)을 그냥 주입받아 쓰면 된다.


3. API 키와 모델 설정 {#3-config}

설정은 src/main/resources/application.yml에 둔다. API 키는 코드·설정 파일에 직접 박지 말고 환경변수로 주입하는 것이 원칙이다. 아래는 OPENAI_API_KEY 환경변수가 있으면 그 값을, 없으면 기본값을 쓰는 형태다.

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}          # 환경변수로 주입 (export OPENAI_API_KEY=sk-...)
      chat:
        options:
          model: ${OPENAI_MODEL:gpt-4o-mini}   # 기본 모델
          temperature: 0.7                      # 0에 가까울수록 일관, 1에 가까울수록 창의적
      embedding:
        options:
          model: text-embedding-3-small         # RAG에서 쓸 임베딩 모델

# 코드에서 참조할 모델 슬러그 모음 (모델 2개)
app:
  models:
    mini: gpt-4o-mini    # 빠르고 저렴 (기본)
    full: gpt-4o         # 더 강력 (비교용)

API 키 노출 주의

API 키가 깃 저장소에 올라가면 즉시 폐기해야 한다. .gitignore로 키 파일을 제외하거나, 위처럼 환경변수(${OPENAI_API_KEY})로만 주입하자. 실수로 커밋했다면 OpenAI 대시보드에서 키를 폐기하고 새로 발급한다.

모델을 2개 등록하는 이유

app.models에 모델 두 개를 등록해 두면, 코드에서 요청마다 모델을 바꿔 응답을 비교할 수 있다. 예를 들어 "빠르고 싼 모델"과 "느리지만 똑똑한 모델"의 답을 나란히 받아 품질·비용을 가늠한다. 실제 교체 방법은 다음 편에서 다룬다.

flowchart LR
    Q["같은 질문"] --> M1["gpt-4o-mini<br/>(빠름·저렴)"]
    Q --> M2["gpt-4o<br/>(강력)"]
    M1 --> R["응답 비교"]
    M2 --> R

OpenAI 호환 API로 다른 제공자 쓰기

OpenRouter·Ollama 등은 OpenAI와 호환되는 API를 제공한다. 이 경우 spring.ai.openai.base-url만 바꾸면 같은 스타터로 다른 제공자의 모델을 쓸 수 있다.

spring:
  ai:
    openai:
      base-url: https://openrouter.ai/api   # 예: OpenRouter
      api-key: ${OPENROUTER_API_KEY}

4. 동작 확인 {#4-smoke-test}

설정이 끝났으면 가장 간단한 채팅 한 줄로 연결을 확인한다. ChatClient.Builder를 주입받아 build()하고 호출하면 된다.

@RestController
public class PingController {

    private final ChatClient chatClient;

    public PingController(ChatClient.Builder builder) {
        this.chatClient = builder.build();   // 자동설정된 빌더로 ChatClient 생성
    }

    @GetMapping("/ping")
    public String ping(@RequestParam String message) {
        return chatClient.prompt().user(message).call().content();
    }
}
# 애플리케이션을 띄운 뒤
curl "http://localhost:8080/ping?message=hello"
# -> Hello! How can I assist you today?

한글·공백이 있는 요청은 URL 인코딩

curl로 한글이나 공백이 든 질문을 보낼 때는 인코딩이 필요하다. --data-urlencode를 쓰면 편하다.

curl -G "http://localhost:8080/ping" --data-urlencode "message=스프링이 뭐야?"

이제 연결이 확인됐다. 다음 편에서 ChatClient와 ChatModel의 차이, 스트리밍, 그리고 등록한 모델 2개를 바꿔 쓰는 법을 본격적으로 다룬다.

➡️ 다음 편: 기본 채팅과 모델 비교

마지막 수정 2026년 10월 22일