최신 트렌드

Spring AI로 LLM 애플리케이션 개발 - RAG부터 Function Calling까지

백엔드 개발자 김승원 2026. 4. 10. 13:15

들어가며

Spring 생태계에서 LLM 기반 애플리케이션을 구축하려면 각 AI 제공자의 SDK를 직접 다루거나, LangChain 같은 Python 프레임워크로 우회해야 했습니다. Spring AI는 이러한 격차를 해소하기 위해 등장한 공식 스프링 프로젝트로, ChatGPT, Claude, Ollama 등 다양한 AI 모델을 Google Gemma 4 완벽 정리 스프링 방식(추상화, DI, 자동 설정)으로 통합할 수 있게 해줍니다. 이번 글에서는 Spring AI의 핵심 개념을 살펴보고, RAG(Retrieval Augmented Generation), Function Calling, 벡터 DB 연동까지 실무에서 바로 활용할 수 있는 예제를 다루겠습니다.

1. Spring AI 핵심 개념

의존성 설정

// build.gradle
plugins {
    id 'org.springframework.boot' version '3.4.0'
}

repositories {
    mavenCentral()
    maven { url 'https://repo.spring.io/milestone' }
}

dependencies {
    // OpenAI 연동
    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0'
    // 또는 Anthropic Claude 연동
    // implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter Meta Llama 4 오픈소스 모델 활용하기:1.0.0'

    // 벡터 DB (PGVector)
    implementation 'org.springframework.ai:spring-ai-pgvector-store-spring-boot-starter:1.0.0'
}

application.yml

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o
          temperature: 0.7
          max-tokens: 2000
    # Anthropic Claude 사용 시
    # anthropic:
    #   api-key: ${ANTHROPIC_API_KEY}
    #   chat:
    #     options:
    #       model: claude-sonnet-4-20250514
    #       temperature: 0.7

ChatClient - 핵심 인터페이스

Spring AI의 가장 중요한 컴포넌트는 ChatClient입니다. Fluent API로 직관적으로 AI와 대화할 수 있습니다.

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder
            .defaultSystem("당신은 백엔드 개발 전문가입니다. 한국어로 답변합니다.")
            .build();
    }

    @PostMapping
    public String chat(@RequestBody ChatRequest request) {
        return chatClient.prompt()
            .user(request.message())
            .call()
            .content();
    }

    // 스트리밍 응답
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chatStream(@RequestParam String message) {
        return chatClient.prompt()
            .user(message)
            .stream()
            .content();
    }
}

public record ChatRequest(String message) {}

구조화된 출력 (Structured Output)

AI 응답을 Java 객체로 자동 파싱할 수 있습니다.

public record BookRecommendation(
    String title,
    String author,
    String reason,
    int rating
) {}

@GetMapping("/recommend")
public List<BookRecommendation> recommendBooks(@RequestParam String genre) {
    return chatClient.prompt()
        .user("" + genre + " 장르에서 추천할 만한 책 3권을 알려주세요.")
        .call()
        .entity(new ParameterizedTypeReference<List<BookRecommendation>>() {});
}

2. RAG (Retrieval Augmented Generation) 구현

RAG는 외부 데이터를 검색하여 AI 모델의 컨텍스트에 주입하는 패턴입니다. 컨텍스트 엔지니어링 패러다임 살펴보기 AI 모델이 학습하지 못한 최신 데이터나 사내 문서를 기반으로 정확한 답변을 생성할 수 있습니다.

RAG 파이프라인 개요

┌──────────┐     ┌───────────┐     ┌──────────────┐
│  문서    │────▶│ Embedding │────▶│  Vector DB   │
│  수집    │     │  변환     │     │  (PGVector)  │
└──────────┘     └───────────┘     └──────┬───────┘
                                          │
┌──────────┐     ┌───────────┐     ┌──────▼───────┐
│  사용자  │────▶│  유사도   │────▶│  관련 문서   │
│  질문    │     │  검색     │     │  Top-K 추출  │
└──────────┘     └───────────┘     └──────┬───────┘
                                          │
                 ┌───────────┐     ┌──────▼───────┐
                 │  AI 응답  │◀────│  컨텍스트 +  │
                 │  생성     │     │  질문 결합   │
                 └───────────┘     └──────────────┘

벡터 DB 설정 (PGVector)

spring:
  ai:
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1536  # OpenAI embedding 차원 수
  datasource:
    url: jdbc:postgresql://localhost:5432/vectordb
    username: postgres
    password: secret

문서 수집 및 임베딩

@Service
@RequiredArgsConstructor
public class DocumentIngestionService {

    private final VectorStore vectorStore;

    // 텍스트 문서 수집
    public void ingestDocuments(List<String> documents) {
        List<Document> docs = documents.stream()
            .map(text -> new Document(text))
            .toList();
        vectorStore.add(docs);
    }

    // 파일에서 문서 수집 (PDF, 텍스트 등)
    public void ingestFromFile(Resource resource) {
        // PDF 읽기
        var reader = new PagePdfDocumentReader(resource);
        List<Document> documents = reader.read();

        // 청크 분할 (토큰 제한을 위해)
        var splitter = new TokenTextSplitter(
            800,    // chunkSize
            200,    // overlap
            5,      // minChunkSizeChars
            10000,  // maxNumChunks
            true    // keepSeparator
        );
        List<Document> chunks = splitter.apply(documents);

        // 메타데이터 추가
        chunks.forEach(doc -> {
            doc.getMetadata().put("source", resource.getFilename());
            doc.getMetadata().put("ingested_at", Instant.now().toString());
        });

        // 벡터 DB에 저장 (자동 임베딩 변환)
        vectorStore.add(chunks);
    }
}

RAG 기반 질의응답 서비스

@Service
@RequiredArgsConstructor
public class RagService {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    public String askWithContext(String question) {
        // 1. 질문과 관련된 문서 검색
        List<Document> relevantDocs = vectorStore.similaritySearch(
            SearchRequest.builder()
                .query(question)
                .topK(5)
                .similarityThreshold(0.7)
                .build()
        );

        // 2. 검색된 문서를 컨텍스트로 구성
        String context = relevantDocs.stream()
            .map(Document::getText)
            .collect(Collectors.joining("\n\n---\n\n"));

        // 3. 컨텍스트와 질문을 결합하여 AI에 전달
        String response = chatClient.prompt()
            .system(s -> s.text("""
                다음 컨텍스트를 참고하여 질문에 답변하세요.
                컨텍스트에 없는 내용은 "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 답변하세요.

                컨텍스트:
                {context}
                """))
            .user(question)
            .call()
            .content();

        return response;
    }
}

// 또는 Spring AI의 QuestionAnswerAdvisor 활용
@Service
public class RagAdvisorService {

    private final ChatClient chatClient;

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

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

3. Function Calling으로 외부 API 연동

Function Calling은 AI 모델이 외부 함수를 호출하여 실시간 데이터를 가져오거나 작업을 수행하는 기능입니다. MCP 완벽 가이드로 AI 에이전트 통합 표준 배우기 Spring AI에서는 @Bean으로 함수를 등록하면 자동으로 Tool로 변환됩니다.

함수 정의 및 등록

// 날씨 조회 함수
@Configuration
public class AiFunctions {

    @Bean
    @Description("지정한 도시의 현재 날씨 정보를 조회합니다")
    public Function<WeatherRequest, WeatherResponse> getCurrentWeather(
            WeatherService weatherService) {
        return request -> weatherService.getWeather(request.city());
    }

    @Bean
    @Description("지정한 주식 종목의 현재가를 조회합니다")
    public Function<StockRequest, StockResponse> getStockPrice(
            StockService stockService) {
        return request -> stockService.getPrice(request.symbol());
    }

    @Bean
    @Description("주문을 생성합니다")
    public Function<OrderRequest, OrderResponse> createOrder(
            OrderService orderService) {
        return request -> orderService.create(request);
    }
}

public record WeatherRequest(String city) {}
public record WeatherResponse(String city, double temperature, String condition) {}
public record StockRequest(String symbol) {}
public record StockResponse(String symbol, double price, double change) {}

Function Calling 활용

@RestController
@RequestMapping("/api/assistant")
public class AssistantController {

    private final ChatClient chatClient;

    public AssistantController(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("당신은 유능한 비서입니다. 날씨, 주식 등의 정보를 조회하여 답변합니다.")
            .defaultTools("getCurrentWeather", "getStockPrice", "createOrder")
            .build();
    }

    @PostMapping("/ask")
    public String ask(@RequestBody String question) {
        // AI 모델이 자동으로 필요한 함수를 판단하여 호출
        // "서울 날씨 어때?" → getCurrentWeather("서울") 자동 호출
        // "삼성전자 주가 알려줘" → getStockPrice("005930") 자동 호출
        return chatClient.prompt()
            .user(question)
            .call()
            .content();
    }
}

동작 흐름

사용자: "서울 날씨와 삼성전자 주가 알려줘"

1. AI 모델이 질문을 분석하고 필요한 함수 결정
2. getCurrentWeather({city: "서울"}) 호출 → {city: "서울", temperature: 18.5, condition: "맑음"}
3. getStockPrice({symbol: "005930"}) 호출 → {symbol: "005930", price: 72400, change: 1.2}
4. AI가 함수 결과를 종합하여 자연어 응답 생성

AI 응답: "현재 서울 날씨는 18.5도로 맑습니다.
          삼성전자(005930)의 현재 주가는 72,400원으로 전일 대비 1.2% 상승했습니다."

4. Prompt Template 활용

@Service
public class CodeReviewService {

    private final ChatClient chatClient;

    @Value("classpath:/prompts/code-review.st")
    private Resource codeReviewPrompt;

    public CodeReviewService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String reviewCode(String code, String language) {
        return chatClient.prompt()
            .user(u -> u
                .text(codeReviewPrompt)  // 외부 템플릿 파일 사용
                .param("code", code)
                .param("language", language)
            )
            .call()
            .content();
    }
}

// src/main/resources/prompts/code-review.st (StringTemplate 형식)
// 다음 {language} 코드를 리뷰해주세요.
// 보안 취약점, 성능 이슈, 코드 스타일을 중점적으로 확인합니다.
//
// ```{language}
// {code}
// ```
//
// 리뷰 결과를 다음 형식으로 작성:
// 1. 심각도 (HIGH/MEDIUM/LOW)
// 2. 위치
// 3. 설명
// 4. 개선 코드

5. 벡터 DB 비교 및 선택

벡터 DB 특징 Spring AI 지원 추천 상황
PGVector PostgreSQL 확장, SQL과 벡터 검색 통합 spring-ai-pgvector-store 이미 PostgreSQL 사용 중일 때
Chroma 경량 벡터 DB, 빠른 프로토타이핑 spring-ai-chroma-store PoC, 소규모 프로젝트
Milvus 분산 벡터 DB, 대규모 처리 spring-ai-milvus-store 대규모 문서, 높은 성능 필요
Redis 인메모리, 낮은 지연 spring-ai-redis-store 실시간 검색, 캐시 활용
Weaviate 하이브리드 검색 (벡터 + 키워드) spring-ai-weaviate-store 복합 검색 필요 시

PGVector 설정 상세

-- PostgreSQL에서 pgvector 확장 활성화
CREATE EXTENSION IF NOT EXISTS vector;

-- Spring AI가 자동 생성하는 테이블 구조
-- vector_store 테이블
-- id UUID PRIMARY KEY
-- content TEXT
-- metadata JSON
-- embedding VECTOR(1536)  -- OpenAI 기준

6. 실무 활용 시나리오: 사내 문서 검색 챗봇

@RestController
@RequestMapping("/api/docs")
@RequiredArgsConstructor
public class DocsChatController {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;
    private final DocumentIngestionService ingestionService;

    // 문서 업로드 및 임베딩
    @PostMapping("/upload")
    public ResponseEntity<String> uploadDocument(
            @RequestParam("file") MultipartFile file) throws IOException {

        Resource resource = new ByteArrayResource(file.getBytes()) {
            @Override
            public String getFilename() {
                return file.getOriginalFilename();
            }
        };

        ingestionService.ingestFromFile(resource);
        return ResponseEntity.ok("문서가 성공적으로 인덱싱되었습니다.");
    }

    // 문서 기반 질의응답
    @PostMapping("/ask")
    public ChatResponse ask(@RequestBody DocQuestion request) {
        // 관련 문서 검색
        List<Document> relevantDocs = vectorStore.similaritySearch(
            SearchRequest.builder()
                .query(request.question())
                .topK(5)
                .filterExpression(
                    new FilterExpressionBuilder()
                        .eq("source", request.source())
                        .build()
                )
                .build()
        );

        String context = relevantDocs.stream()
            .map(Document::getText)
            .collect(Collectors.joining("\n\n"));

        List<String> sources = relevantDocs.stream()
            .map(doc -> (String) doc.getMetadata().get("source"))
            .distinct()
            .toList();

        String answer = chatClient.prompt()
            .system("""
                사내 문서를 기반으로 답변하는 AI 어시스턴트입니다.
                반드시 제공된 컨텍스트만을 근거로 답변하세요.
                """)
            .user(u -> u.text("""
                컨텍스트: {context}

                질문: {question}
                """)
                .param("context", context)
                .param("question", request.question())
            )
            .call()
            .content();

        return new ChatResponse(answer, sources);
    }
}

public record DocQuestion(String question, String source) {}
public record ChatResponse(String answer, List<String> sources) {}

7. 주의사항 및 Best Practice

  • API 키 관리: 환경 변수 또는 Vault를 통해 관리하세요. application.yml에 직접 넣지 마세요.
  • 비용 관리: 토큰 사용량을 모니터링하세요. 불필요하게 긴 컨텍스트는 비용을 증가시킵니다. 청크 크기와 Top-K를 적절히 조절하세요.
  • 환각(Hallucination) 방어: RAG에서 컨텍스트에 없는 내용은 답변하지 않도록 시스템 프롬프트에 명시하세요. 출처를 함께 반환하면 검증이 쉬워집니다.
  • 임베딩 모델 일관성: 문서 저장과 검색에 동일한 임베딩 모델을 사용해야 합니다. 모델 변경 시 재임베딩이 필요합니다.
  • 청크 전략: 문서 특성에 맞는 청크 크기를 실험적으로 결정하세요. 일반적으로 500-1000 토큰이 적절합니다. 오버랩은 20% 정도를 권장합니다.
  • 에러 처리: AI API 호출 실패, 타임아웃, 토큰 한도 초과 등에 대한 예외 처리와 재시도 전략을 구현하세요.

마치며

Spring AI는 자바/스프링 개발자에게 AI 통합의 진입 장벽을 크게 낮춰주는 프레임워크입니다. 핵심 포인트를 정리합니다.

  • ChatClient는 Spring AI의 중심으로, Fluent API로 직관적인 AI 상호작용을 제공합니다. 구조화된 출력(Entity 변환)까지 지원합니다.
  • RAG는 사내 문서, 도메인 데이터를 AI에 주입하는 핵심 패턴입니다. VectorStore와 QuestionAnswerAdvisor로 쉽게 구현할 수 있습니다.
  • Function Calling은 AI 모델이 외부 API를 호출하게 해주어, 실시간 데이터 조회나 작업 수행이 가능합니다. @Bean으로 함수를 등록하면 자동으로 Tool로 변환됩니다.
  • 벡터 DB는 PGVector(PostgreSQL 사용 중), Chroma(프로토타입), Milvus(대규모) 중 상황에 맞게 선택하세요.
  • 프로덕션 적용 시에는 비용 관리, 환각 방어, 에러 처리를 반드시 고려하세요.

Spring AI는 아직 빠르게 발전 중인 프로젝트이므로 API 변경이 있을 수 있습니다. 하지만 Spring의 철학(추상화, 표준화, 생산성)이 AI 영역에도 적용된다는 점에서, Java 백엔드 개발자가 AI 애플리케이션을 구축하는 가장 자연스러운 선택이 될 것입니다. 2026 백엔드 기술 스택에서의 AI 통합 트렌드