들어가며
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 통합 트렌드
'최신 트렌드' 카테고리의 다른 글
| 2026년 4월 AI 코딩·모델 총정리 - Cursor 3, Windsurf, Claude Code 그리고 프론티어 모델 3파전 (2) | 2026.04.16 |
|---|---|
| Claude Code 데스크톱 앱 대규모 리디자인 총정리 - 병렬 세션부터 Routines 자동화까지 (1) | 2026.04.16 |
| 2026 백엔드 기술 스택 총정리 - 현업 개발자의 선택과 트렌드 (1) | 2026.04.10 |
| AI 시대의 백엔드 개발자 생존 전략 - 무엇을 배우고 어떻게 적응할 것인가 (1) | 2026.04.10 |
| MCP(Model Context Protocol) 완벽 가이드 - AI 에이전트 통합의 새로운 표준 (0) | 2026.04.10 |