Spring AI 입문 — 자바 개발자를 위한 LLM 통합 가이드

자바 개발자가 AI 기능을 통합하려면 어디서부터?

Python에는 LangChain이, TypeScript에는 Vercel AI SDK가 있습니다. 그렇다면 Java/Spring 생태계 에서 LLM을 통합하는 표준적인 방법은 뭘까요?

Spring AI 가 그 답입니다. Spring 팀이 공식으로 개발하는 프로젝트로, Spring Boot의 자동 설정 철학을 그대로 AI 영역에 적용합니다. REST API를 직접 호출하는 대신, ChatClient라는 추상화를 통해 OpenAI, Anthropic, Ollama 등 다양한 모델을 동일한 인터페이스로 사용할 수 있습니다.


의존성 설정

Spring Boot 프로젝트에 Spring AI를 추가합니다. 사용할 모델에 따라 의존성이 다릅니다.

Gradle (build.gradle)

GROOVY
// BOM 추가
dependencyManagement {
    imports {
        mavenBom "org.springframework.ai:spring-ai-bom:1.0.0"
    }
}

dependencies {
    // OpenAI 사용 시
    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'

    // Anthropic(Claude) 사용 시
    // implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter'

    // 로컬 모델 (Ollama) 사용 시
    // implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter'
}

Maven (pom.xml)

XML
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

application.yml

YAML
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o
          temperature: 0.7

API 키는 환경 변수나 Vault로 관리하세요. application.yml에 직접 넣지 마세요.


ChatClient — LLM 호출의 핵심

ChatClient는 Spring AI의 중심 인터페이스입니다. Fluent API로 프롬프트를 구성합니다.

기본 호출

JAVA
@Service
public class AiService {
    private final ChatClient chatClient;

    public AiService(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    // 단순 텍스트 응답
    public String askQuestion(String question) {
        return chatClient.prompt()
                .user(question)
                .call()
                .content();  // 텍스트 응답 추출
    }
}

시스템 프롬프트 설정

JAVA
public String translate(String text, String targetLanguage) {
    return chatClient.prompt()
            .system("당신은 전문 번역가입니다. 원문의 뉘앙스를 살려 번역하세요.")
            .user("다음 텍스트를 " + targetLanguage + "로 번역해주세요: " + text)
            .call()
            .content();
}

대화 맥락 유지

JAVA
public String chat(List<Message> conversationHistory, String newMessage) {
    return chatClient.prompt()
            .messages(conversationHistory)  // 이전 대화 이력
            .user(newMessage)               // 새 메시지
            .call()
            .content();
}

프롬프트 템플릿

프롬프트를 하드코딩하지 않고, 템플릿 으로 관리합니다. Spring의 Resource 시스템과 통합됩니다.

리소스 기반 템플릿

TEXT
# src/main/resources/prompts/review.st
다음 코드를 리뷰해주세요.

언어: {language}
중점 사항: {focus}

```java
{code}

개선 사항을 3가지 이내로 제안해주세요.

PLAINTEXT

```java
@Service
public class CodeReviewService {
    private final ChatClient chatClient;

    @Value("classpath:prompts/review.st")
    private Resource reviewPromptResource;

    public String reviewCode(String code, String language) {
        PromptTemplate template = new PromptTemplate(reviewPromptResource);
        Prompt prompt = template.create(Map.of(
            "language", language,
            "focus", "성능, 보안, 가독성",
            "code", code
        ));

        return chatClient.prompt(prompt)
                .call()
                .content();
    }
}

Output Parser — LLM 응답을 Java 객체로

LLM의 텍스트 응답을 구조화된 Java 객체 로 변환합니다.

BeanOutputParser — Record/POJO로 매핑

JAVA
// 응답을 매핑할 Record
public record MovieRecommendation(
    String title,
    String genre,
    int year,
    String reason
) {}

@Service
public class RecommendationService {
    private final ChatClient chatClient;

    public MovieRecommendation recommend(String mood) {
        return chatClient.prompt()
                .user("현재 기분: " + mood + ". 영화 한 편을 추천해주세요.")
                .call()
                .entity(MovieRecommendation.class);  // 자동으로 JSON 파싱
    }
}

entity() 메서드가 내부적으로:

  1. 프롬프트에 응답 포맷 지시를 추가하고
  2. LLM 응답을 파싱하여 Java 객체로 변환합니다

리스트로 매핑

JAVA
public List<MovieRecommendation> recommendMultiple(String mood) {
    return chatClient.prompt()
            .user("현재 기분: " + mood + ". 영화 3편을 추천해주세요.")
            .call()
            .entity(new ParameterizedTypeReference<List<MovieRecommendation>>() {});
}

스트리밍 응답

긴 응답을 실시간으로 받아 처리합니다.

JAVA
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamAnswer(@RequestParam String question) {
    return chatClient.prompt()
            .user(question)
            .stream()
            .content();  // Flux<String> 반환
}

WebFlux와 자연스럽게 통합되어 SSE(Server-Sent Events)로 클라이언트에 스트리밍할 수 있습니다.


Embedding — 텍스트를 벡터로

임베딩(Embedding) 은 텍스트를 숫자 벡터로 변환하는 것입니다. 유사한 의미의 텍스트는 유사한 벡터를 가지므로, 의미 기반 검색 의 기초가 됩니다.

JAVA
@Service
public class EmbeddingService {
    private final EmbeddingModel embeddingModel;

    public EmbeddingService(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
    }

    // 텍스트 → 벡터 변환
    public float[] embed(String text) {
        return embeddingModel.embed(text);
    }

    // 두 텍스트의 유사도 계산
    public double similarity(String text1, String text2) {
        float[] vec1 = embeddingModel.embed(text1);
        float[] vec2 = embeddingModel.embed(text2);
        return cosineSimilarity(vec1, vec2);
    }
}

VectorStore — 벡터 저장 및 검색

임베딩된 벡터를 저장하고, 유사도 기반 검색을 수행합니다.

pgvector (PostgreSQL 확장)

YAML
# application.yml
spring:
  ai:
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1536
JAVA
@Service
public class DocumentService {
    private final VectorStore vectorStore;

    // 문서 저장
    public void storeDocuments(List<String> texts) {
        List<Document> documents = texts.stream()
            .map(text -> new Document(text))
            .toList();
        vectorStore.add(documents);
    }

    // 유사 문서 검색
    public List<Document> search(String query) {
        return vectorStore.similaritySearch(
            SearchRequest.builder()
                .query(query)
                .topK(5)              // 상위 5개
                .similarityThreshold(0.7)  // 유사도 70% 이상
                .build()
        );
    }
}

지원하는 VectorStore 구현체

VectorStore특징
pgvectorPostgreSQL 확장, 기존 DB에 추가 가능
RedisUpstash/Redis Stack, 빠른 검색
Milvus대규모 벡터 검색 전문
Chroma경량, 로컬 개발에 적합
Pinecone완전 관리형 서비스
WeaviateGraphQL 기반 검색

RAG — 검색 증강 생성

RAG(Retrieval-Augmented Generation) 는 LLM이 모르는 정보(사내 문서, 최신 데이터)를 검색해서 프롬프트에 주입 하는 패턴입니다.

PLAINTEXT
사용자 질문

[1] 질문을 벡터로 변환 (Embedding)

[2] VectorStore에서 유사 문서 검색 (Retrieval)

[3] 검색된 문서 + 원래 질문을 LLM에 전달 (Augmented Generation)

정확한 응답

Spring AI의 QuestionAnswerAdvisor

Spring AI는 RAG를 Advisor 패턴으로 깔끔하게 제공합니다.

JAVA
@Service
public class RagService {
    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    public RagService(ChatClient.Builder builder, VectorStore vectorStore) {
        this.vectorStore = vectorStore;
        this.chatClient = builder
            .defaultAdvisors(
                new QuestionAnswerAdvisor(vectorStore,
                    SearchRequest.builder().topK(5).build())
            )
            .build();
    }

    public String askAboutDocs(String question) {
        // QuestionAnswerAdvisor가 자동으로:
        // 1. question을 벡터로 변환
        // 2. VectorStore에서 관련 문서 검색
        // 3. 검색 결과를 프롬프트에 추가
        // 4. LLM에 전달
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}

문서 로딩 파이프라인

JAVA
@Component
public class DocumentLoader {
    private final VectorStore vectorStore;

    // 텍스트 파일을 읽어서 VectorStore에 저장
    public void loadDocuments(Resource... resources) {
        // 1. 문서 읽기
        var reader = new TextReader(resources);
        List<Document> documents = reader.read();

        // 2. 청크로 분할 (긴 문서를 적절한 크기로 자름)
        var splitter = new TokenTextSplitter();
        List<Document> chunks = splitter.split(documents);

        // 3. VectorStore에 저장 (임베딩은 자동)
        vectorStore.add(chunks);
    }
}

Function Calling — LLM이 Java 메서드를 호출

LLM이 필요할 때 사전 정의된 함수를 호출 할 수 있게 합니다.

JAVA
// 함수 정의
@Bean
@Description("주어진 도시의 현재 날씨를 조회합니다")
public Function<WeatherRequest, WeatherResponse> currentWeather() {
    return request -> {
        // 실제 날씨 API 호출
        return weatherApi.getWeather(request.city());
    };
}

public record WeatherRequest(String city) {}
public record WeatherResponse(String city, double temperature, String condition) {}
JAVA
// 함수를 ChatClient에 등록
public String askAboutWeather(String question) {
    return chatClient.prompt()
            .user(question)
            .functions("currentWeather")  // 함수 이름 등록
            .call()
            .content();
}

// "서울 날씨 어때?" → LLM이 currentWeather("서울") 호출 → 결과를 바탕으로 응답 생성

LLM이 직접 API를 호출하는 게 아닙니다. LLM이 "이 함수를 이 인자로 호출해야 한다"고 판단하면, Spring AI가 실제 함수를 실행하고 결과를 다시 LLM에 전달합니다.


멀티 모델 지원

Spring AI는 모델을 인터페이스로 추상화하므로, 용도에 따라 다른 모델을 사용할 수 있습니다.

JAVA
@Configuration
public class AiConfig {

    // 일반 채팅용 — 빠르고 저렴한 모델
    @Bean("chatModel")
    public ChatClient chatClient(OpenAiChatModel model) {
        return ChatClient.builder(model)
            .defaultOptions(ChatOptionsBuilder.builder()
                .model("gpt-4o-mini")
                .build())
            .build();
    }

    // 분석/추론용 — 고성능 모델
    @Bean("reasoningModel")
    public ChatClient reasoningClient(AnthropicChatModel model) {
        return ChatClient.builder(model)
            .defaultOptions(ChatOptionsBuilder.builder()
                .model("claude-sonnet-4-6")
                .build())
            .build();
    }
}

Ollama로 로컬 개발

외부 API 없이 로컬에서 개발하려면 Ollama 를 사용합니다.

BASH
# Ollama 설치 후
ollama pull llama3.2
YAML
spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        model: llama3.2

코드는 전혀 바꿀 필요 없습니다. ChatClient 인터페이스가 동일하니까요. 의존성만 spring-ai-ollama-spring-boot-starter로 바꾸면 됩니다.


실전 아키텍처 예시: 사내 문서 Q&A 봇

PLAINTEXT
[사용자] "배포 절차가 어떻게 되나요?"

[Controller] REST API → AiService

[QuestionAnswerAdvisor]
  1. 질문 임베딩 (EmbeddingModel)
  2. VectorStore에서 관련 사내 문서 검색
  3. 검색된 문서 + 질문을 프롬프트에 추가

[ChatClient] → OpenAI API (또는 Ollama)

[응답] "배포 절차는 다음과 같습니다: 1. develop 브랜치에서..."
JAVA
@RestController
@RequestMapping("/api/qa")
public class QaController {
    private final RagService ragService;

    @PostMapping
    public ResponseEntity<QaResponse> ask(@RequestBody QaRequest request) {
        String answer = ragService.askAboutDocs(request.question());
        return ResponseEntity.ok(new QaResponse(answer));
    }
}

주의할 점

비용 관리

LLM API 호출은 토큰 단위로 과금됩니다. RAG에서 너무 많은 문서를 컨텍스트에 넣으면 비용이 급증합니다.

JAVA
// topK와 similarityThreshold로 검색 결과를 제한
SearchRequest.builder()
    .query(question)
    .topK(3)                  // 최대 3개 문서만
    .similarityThreshold(0.8) // 유사도 80% 이상만
    .build();

환각(Hallucination) 대응

LLM은 모르는 것도 그럴듯하게 답합니다. RAG에서 관련 문서가 없으면 "모르겠다"고 답하도록 시스템 프롬프트에 명시하세요.

Spring AI 버전

Spring AI는 빠르게 발전하고 있어 API가 변경될 수 있습니다. BOM 버전을 고정하고, 업그레이드 시 릴리스 노트를 확인하세요.


정리

Spring AI는 Java/Spring 개발자가 익숙한 패턴으로 LLM을 통합 할 수 있게 해줍니다. ChatClient → 프롬프트 템플릿 → Output Parser → VectorStore → RAG 순서로 점진적으로 도입할 수 있습니다.

기능Spring AI 컴포넌트용도
LLM 호출ChatClient기본 텍스트 생성
프롬프트 관리PromptTemplate템플릿 기반 프롬프트
응답 구조화entity() / OutputParserJSON → Java 객체
벡터 검색VectorStore의미 기반 문서 검색
RAGQuestionAnswerAdvisor문서 기반 Q&A
함수 호출@Bean + functions()LLM → Java 메서드 실행

공부하면서 느낀 건, Spring AI의 가장 큰 장점이 기존 Spring 생태계와의 자연스러운 통합 이라는 점입니다. DI, 자동 설정, Actuator 모니터링 등 이미 알고 있는 패턴을 그대로 AI 영역에 적용할 수 있다는 것이 Python 기반 도구들과의 차별점입니다.

댓글 로딩 중...