Spring AI란 무엇인가?
Spring AI는 Spring 생태계에서 공식적으로 제공하는 AI 통합 프레임워크입니다. 기존에 LLM을 Java 애플리케이션에 연동하려면 각 제공사의 REST API를 직접 호출하거나 서드파티 라이브러리에 의존해야 했습니다. Spring AI는 이러한 복잡성을 추상화하여, Spring 개발자에게 익숙한 방식으로 AI 기능을 통합할 수 있게 해줍니다.
Spring Data가 다양한 데이터베이스를 통합 인터페이스로 다루듯이, Spring AI는 OpenAI, Anthropic, Ollama, Azure OpenAI 등 다양한 AI 모델 제공사를 일관된 API로 사용할 수 있게 합니다. 2026년 현재 정식 GA 버전이 릴리스되어 프로덕션 환경에서도 안정적으로 사용할 수 있습니다.
프로젝트 설정
Spring Initializr에서 Spring AI 의존성을 추가하거나, 기존 프로젝트에 수동으로 설정할 수 있습니다. 여기서는 Gradle 기준으로 설명합니다.
build.gradle 설정
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.2'
id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
// Spring AI BOM
implementation platform('org.springframework.ai:spring-ai-bom:1.0.0')
// OpenAI 연동
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
// Anthropic 연동 (필요시)
// implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter'
// Ollama 로컬 모델 연동 (필요시)
// implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter'
// 벡터 스토어 (pgvector)
implementation 'org.springframework.ai:spring-ai-pgvector-store-spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.postgresql:postgresql'
}
application.yml 설정
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.7
max-tokens: 2000
# Anthropic 사용 시
# anthropic:
# api-key: ${ANTHROPIC_API_KEY}
# chat:
# options:
# model: claude-sonnet-4-20250514
# max-tokens: 4096
ChatClient 기본 사용법
Spring AI의 핵심 컴포넌트는 ChatClient입니다. Spring의 RestClient나 WebClient와 유사한 빌더 패턴으로 설계되어 있어, Spring 개발자라면 직관적으로 사용할 수 있습니다.
간단한 채팅 구현
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("당신은 친절한 기술 상담 전문가입니다. 한국어로 답변하세요.")
.build();
}
@PostMapping
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest request) {
String response = chatClient.prompt()
.user(request.getMessage())
.call()
.content();
return ResponseEntity.ok(new ChatResponse(response));
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
}
ChatModel 직접 사용
ChatClient가 고수준 API라면, ChatModel은 저수준 API입니다. 세밀한 제어가 필요한 경우에 사용합니다.
@Service
public class AdvancedChatService {
private final ChatModel chatModel;
public AdvancedChatService(ChatModel chatModel) {
this.chatModel = chatModel;
}
public String generateWithOptions(String userMessage) {
ChatResponse response = chatModel.call(
new Prompt(
List.of(
new SystemMessage("코드 리뷰 전문가입니다."),
new UserMessage(userMessage)
),
OpenAiChatOptions.builder()
.model("gpt-4o")
.temperature(0.3)
.topP(0.9)
.build()
)
);
return response.getResult().getOutput().getText();
}
}
프롬프트 템플릿 활용
하드코딩된 프롬프트 대신 템플릿을 사용하면 프롬프트를 외부에서 관리하고, 변수를 동적으로 주입할 수 있습니다.
@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 language, String code) {
return chatClient.prompt()
.user(u -> u
.text(codeReviewPrompt)
.param("language", language)
.param("code", code)
)
.call()
.content();
}
}
src/main/resources/prompts/code-review.st 파일 내용:
다음 {language} 코드를 리뷰해 주세요.
리뷰 기준:
1. 코드 품질과 가독성
2. 잠재적 버그
3. 성능 개선 가능성
4. 보안 취약점
코드:
```
{code}
```
개선 사항을 구체적인 코드 예시와 함께 한국어로 설명해 주세요.
{변수명}으로 변수를 삽입하며, 조건문이나 반복문도 지원됩니다.
Structured Output: JSON 파싱
LLM의 응답을 자유 텍스트가 아닌 구조화된 Java 객체로 받을 수 있습니다. 이를 통해 AI 응답을 후속 비즈니스 로직에서 안정적으로 활용할 수 있습니다.
// 응답을 매핑할 Record 정의
public record CodeAnalysis(
String summary,
List<String> issues,
List<Suggestion> suggestions,
int qualityScore
) {}
public record Suggestion(
String title,
String description,
String priority // HIGH, MEDIUM, LOW
) {}
@Service
public class CodeAnalysisService {
private final ChatClient chatClient;
public CodeAnalysisService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public CodeAnalysis analyzeCode(String code) {
return chatClient.prompt()
.user("다음 코드를 분석하고 품질 점수를 매겨주세요:\n" + code)
.call()
.entity(CodeAnalysis.class); // 자동 JSON 파싱
}
// 리스트로 받기
public List<CodeAnalysis> analyzeBatch(List<String> codes) {
return chatClient.prompt()
.user("다음 코드들을 각각 분석해주세요:\n" +
String.join("\n---\n", codes))
.call()
.entity(new ParameterizedTypeReference<List<CodeAnalysis>>() {});
}
}
Function Calling 구현
Function Calling은 LLM이 사전에 정의된 함수를 호출하도록 하여, 외부 시스템과 상호작용하는 기능입니다. 날씨 조회, 데이터베이스 검색, API 호출 등을 AI 대화 흐름 안에서 자연스럽게 처리할 수 있습니다.
함수 정의와 등록
@Configuration
public class AiFunctionConfig {
@Bean
@Description("사용자의 주문 상태를 조회합니다")
public Function<OrderStatusRequest, OrderStatusResponse> getOrderStatus(
OrderService orderService) {
return request -> {
Order order = orderService.findByOrderNumber(request.orderNumber());
return new OrderStatusResponse(
order.getOrderNumber(),
order.getStatus().getDisplayName(),
order.getEstimatedDeliveryDate()
);
};
}
@Bean
@Description("상품을 검색합니다. 카테고리와 가격 범위로 필터링 가능합니다")
public Function<ProductSearchRequest, List<ProductSearchResponse>> searchProducts(
ProductService productService) {
return request -> productService.search(
request.keyword(),
request.category(),
request.minPrice(),
request.maxPrice()
).stream()
.map(p -> new ProductSearchResponse(p.getName(), p.getPrice(), p.getImageUrl()))
.toList();
}
}
// 요청/응답 Record
public record OrderStatusRequest(String orderNumber) {}
public record OrderStatusResponse(String orderNumber, String status, LocalDate estimatedDelivery) {}
public record ProductSearchRequest(String keyword, String category, Integer minPrice, Integer maxPrice) {}
Function Calling 호출
@RestController
@RequestMapping("/api/assistant")
public class ShoppingAssistantController {
private final ChatClient chatClient;
public ShoppingAssistantController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("당신은 온라인 쇼핑몰 고객 지원 어시스턴트입니다.")
.defaultFunctions("getOrderStatus", "searchProducts")
.build();
}
@PostMapping
public String assist(@RequestBody String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
// "주문번호 ORD-2026-001 상태 알려줘"라고 하면
// 자동으로 getOrderStatus 함수가 호출되고 결과를 응답에 반영합니다
}
}
벡터 스토어 연동 (pgvector)
벡터 스토어는 텍스트를 임베딩 벡터로 변환하여 저장하고, 의미적으로 유사한 문서를 검색하는 데 사용됩니다. Spring AI는 pgvector, Chroma, Milvus, Pinecone 등 다양한 벡터 스토어를 지원합니다.
pgvector 설정
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/vectordb
username: postgres
password: secret
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
문서 저장과 검색
@Service
public class DocumentService {
private final VectorStore vectorStore;
public DocumentService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void storeDocuments(List<String> texts) {
List<Document> documents = texts.stream()
.map(text -> new Document(text, Map.of("source", "manual")))
.toList();
vectorStore.add(documents);
}
public List<Document> searchSimilar(String query, int topK) {
return vectorStore.similaritySearch(
SearchRequest.builder()
.query(query)
.topK(topK)
.similarityThreshold(0.7)
.build()
);
}
}
RAG (Retrieval-Augmented Generation) 구현
RAG는 벡터 스토어에서 관련 문서를 검색한 뒤, 그 문서를 컨텍스트로 포함하여 LLM에 질문하는 패턴입니다. 이를 통해 LLM이 알지 못하는 사내 문서, 제품 매뉴얼, FAQ 등을 기반으로 정확한 답변을 생성할 수 있습니다.
@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
.defaultSystem("제공된 컨텍스트를 기반으로만 답변하세요. "
+ "컨텍스트에 없는 정보는 '해당 정보를 찾을 수 없습니다'라고 답하세요.")
.defaultAdvisors(
new QuestionAnswerAdvisor(vectorStore,
SearchRequest.builder()
.topK(5)
.similarityThreshold(0.65)
.build())
)
.build();
}
public String askWithContext(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
// 문서 로딩: PDF, 마크다운 등을 벡터 스토어에 적재
@Component
public class DocumentLoader {
private final VectorStore vectorStore;
@EventListener(ApplicationReadyEvent.class)
public void loadDocuments() {
// PDF 문서 로딩
var pdfReader = new PagePdfDocumentReader("classpath:docs/manual.pdf");
var textSplitter = new TokenTextSplitter(800, 200, 5, 1000, true);
List<Document> chunks = textSplitter.apply(pdfReader.get());
vectorStore.add(chunks);
}
}
에러 핸들링과 재시도 전략
AI API 호출은 네트워크 문제, Rate Limit, 타임아웃 등 다양한 실패 요인이 있습니다. 안정적인 운영을 위해 체계적인 에러 핸들링이 필수입니다.
@Configuration
public class AiRetryConfig {
@Bean
public RetryTemplate aiRetryTemplate() {
return RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(1000, 2.0, 10000)
.retryOn(List.of(
ResourceAccessException.class, // 네트워크 오류
HttpServerErrorException.class // 5xx 에러
))
.build();
}
}
@Service
public class ResilientChatService {
private final ChatClient chatClient;
private final RetryTemplate retryTemplate;
public String chatWithRetry(String message) {
return retryTemplate.execute(context -> {
if (context.getRetryCount() > 0) {
log.warn("AI 호출 재시도 중: 시도 {}회", context.getRetryCount() + 1);
}
return chatClient.prompt()
.user(message)
.call()
.content();
}, context -> {
log.error("AI 호출 최종 실패", context.getLastThrowable());
return "죄송합니다. 일시적인 문제가 발생했습니다. 잠시 후 다시 시도해주세요.";
});
}
}
실무 아키텍처 예시
실제 프로덕션 환경에서 Spring AI를 활용할 때 권장하는 아키텍처 구성입니다.
┌─────────────────────────────────────────────────┐
│ 클라이언트 │
└─────────────┬───────────────────────────────────┘
│ REST / WebSocket
┌─────────────▼───────────────────────────────────┐
│ API Gateway (Spring Cloud Gateway) │
│ - Rate Limiting │
│ - 인증/인가 │
└─────────────┬───────────────────────────────────┘
│
┌─────────────▼───────────────────────────────────┐
│ AI Service (Spring Boot + Spring AI) │
│ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ChatClient│ │VectorStore│ │FunctionCalling│ │
│ └────┬─────┘ └─────┬─────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌────▼─────────────▼──────────────▼───────┐ │
│ │ Advisor Chain (미들웨어) │ │
│ │ - 로깅 / 감사 추적 │ │
│ │ - 컨텐츠 필터링 │ │
│ │ - 토큰 사용량 추적 │ │
│ └──────────────────────────────────────────┘ │
└─────────────┬───────────────────────────────────┘
│
┌─────────┼──────────┐
▼ ▼ ▼
┌───────┐ ┌──────┐ ┌─────────┐
│OpenAI │ │Claude│ │ Ollama │
│ API │ │ API │ │(로컬LLM)│
└───────┘ └──────┘ └─────────┘
모델 전환 전략
@Service
public class MultiModelService {
private final ChatClient primaryClient; // OpenAI
private final ChatClient fallbackClient; // Ollama (로컬)
public MultiModelService(
@Qualifier("openAiChatModel") ChatModel primaryModel,
@Qualifier("ollamaChatModel") ChatModel fallbackModel) {
this.primaryClient = ChatClient.builder(primaryModel).build();
this.fallbackClient = ChatClient.builder(fallbackModel).build();
}
public String chat(String message) {
try {
return primaryClient.prompt().user(message).call().content();
} catch (Exception e) {
log.warn("Primary 모델 실패, Fallback으로 전환: {}", e.getMessage());
return fallbackClient.prompt().user(message).call().content();
}
}
}
마무리
Spring AI는 Java/Spring 개발자가 AI 기능을 통합하는 진입 장벽을 크게 낮춰줍니다. ChatClient의 직관적인 API, Structured Output을 통한 타입 안전한 응답 처리, Function Calling을 통한 외부 시스템 연동, 그리고 벡터 스토어 기반 RAG 구현까지, 실무에서 필요한 대부분의 AI 통합 패턴을 프레임워크 수준에서 지원합니다.
가장 중요한 것은 추상화 덕분에 모델 제공사를 쉽게 전환할 수 있다는 점입니다. OpenAI에서 시작하되, 비용이나 성능 요구사항에 따라 Anthropic이나 로컬 Ollama로 유연하게 전환하는 전략을 세울 수 있습니다. 점진적으로 도입하면서 프롬프트 엔지니어링과 RAG 품질을 개선해 나가는 것이 성공적인 AI 통합의 핵심입니다.
10년차 풀스택 개발자. Spring Boot, Flutter, AI 등 실무 경험을 기록합니다.
GitHub →
💬 댓글