[Spring AI] 개요: ChatClient, ChatModel, Prompt
정의
Spring AI 는 LLM (OpenAI, Anthropic, Vertex AI, Bedrock, Ollama 등) 을 Spring 스타일로 사용하게 해 주는 abstraction. 2024 년 GA (1.0), 2025-2026 년에 1.x 안정 시리즈 진행 중.
핵심 추상화:
ChatClient: 고수준 API, fluent DSL, advisor / tool / structured output 통합ChatModel: 저수준 API, 단일 호출, provider-specific tuningPrompt/PromptTemplate: 메시지 + system / user / assistant roleEmbedding/VectorStore: RAG 의 양대 기반 (별도 페이지: spring-ai-rag)Advisor: ChatClient 호출 chain 에 끼어드는 plugin (RAG, 로깅, safe guard)@Tool: function calling, 모델이 자바 메서드를 직접 호출ChatMemory: 대화 이력 관리
LangChain4j 와 경쟁. Spring 사용자라면 통합 (@SpringBootApplication, @Autowired, 자동 설정) 측면에서 Spring AI 가 자연스러움.
의존성
// build.gradle.kts (Spring Boot 3.3+)
implementation("org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0")
// 또는: spring-ai-anthropic-spring-boot-starter, -ollama-, -bedrock-, -vertex-ai-, -mistral-, -azure-openai-
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
temperature: 0.7
자동 설정이 ChatModel, ChatClient.Builder 같은 Bean 을 등록.
ChatClient: 가장 흔한 사용
@RestController
public class JokeController {
private final ChatClient chatClient;
public JokeController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/joke")
public String joke(@RequestParam String topic) {
return chatClient.prompt()
.user(u -> u.text("Tell me a short joke about {topic}").param("topic", topic))
.call()
.content();
}
}GET /joke?topic=cats
→ "Why don't cats play poker in the jungle? Too many cheetahs."prompt() → user/system → call() → content() 의 fluent DSL. .stream() 으로 SSE 토큰 스트리밍도 가능.
System 메시지와 PromptTemplate
String response = chatClient.prompt()
.system("당신은 친절한 Spring 강사입니다. 답변은 한국어, 코드 포함.")
.user(u -> u.text("@Transactional 의 propagation 7가지를 표로 정리해 주세요"))
.call()
.content();
PromptTemplate 으로 분리하면 재사용:
@Component
class ReviewService {
private final ChatClient client;
private final PromptTemplate REVIEW = new PromptTemplate("""
다음 코드를 리뷰해 주세요. 카테고리: 가독성, 성능, 보안.
```{language}
{code}
```
""");
public String review(String language, String code) {
return client.prompt(REVIEW.create(Map.of("language", language, "code", code)))
.call()
.content();
}
}
ChatModel (저수준)
provider-specific 옵션이나 prompt 재사용이 필요 없는 단발성 호출:
@Service
class SimpleTextService {
private final ChatModel model; // OpenAiChatModel, AnthropicChatModel 등
public String summarize(String input) {
Prompt prompt = new Prompt(
List.of(
new SystemMessage("문서 요약 어시스턴트"),
new UserMessage(input)
),
OpenAiChatOptions.builder().withTemperature(0.0).withMaxTokens(200).build()
);
return model.call(prompt).getResult().getOutput().getContent();
}
}
ChatClient 는 ChatModel 위에 얹은 편의 layer. 대부분 ChatClient 권장.
Structured Output
LLM 응답을 자바 객체로 직접 매핑:
record Recipe(String name, List<String> ingredients, List<String> steps) {}
Recipe recipe = chatClient.prompt()
.user("Suggest a simple pasta recipe")
.call()
.entity(Recipe.class);
내부적으로 schema 를 prompt 에 추가하고 JSON 응답을 파싱. 실패 시 IllegalStateException 또는 retry advisor 와 조합.
List, Map 도 지원:
List<String> topics = chatClient.prompt()
.user("List 5 trending Spring topics")
.call()
.entity(new ParameterizedTypeReference<List<String>>() {});
Function / Tool Calling
모델이 자바 메서드를 직접 호출:
@Component
class WeatherTools {
@Tool(description = "Get current weather for a city in Celsius")
public String getCurrentWeather(@ToolParam(description = "City name") String city) {
return weatherApi.fetch(city); // 실제 API
}
}
@Service
class AssistantService {
private final ChatClient client;
private final WeatherTools weatherTools;
public String ask(String question) {
return client.prompt()
.user(question)
.tools(weatherTools)
.call()
.content();
}
}
LLM 이 질문에 따라 getCurrentWeather("Seoul") 호출 → 결과를 받아 최종 응답 생성. 여러 함수 호출도 자동 chaining.
@Tool 의 description 이 모델에게 보내는 메타데이터. 명확하게 작성.
Streaming
Flux<String> stream = chatClient.prompt()
.user("Write a long essay on JVM")
.stream()
.content();
stream.subscribe(System.out::print);
SSE 응답을 컨트롤러에서 그대로 클라이언트에 전달 (WebFlux 통합).
Advisor: chain 에 끼어드는 plugin
ChatClient 호출 전후로 동작. 내장 advisor:
| Advisor | 역할 |
|---|---|
MessageChatMemoryAdvisor | 대화 이력 자동 첨부 (ChatMemory) |
PromptChatMemoryAdvisor | 메모리 + system prompt 결합 |
QuestionAnswerAdvisor | RAG (vector store 검색 + context 첨부, spring-ai-rag 참고) |
SafeGuardAdvisor | 입력 / 출력 검열 (금칙어, profanity) |
SimpleLoggerAdvisor | 요청 / 응답 로깅 |
ChatClient client = builder
.defaultAdvisors(new SimpleLoggerAdvisor(), memoryAdvisor, ragAdvisor)
.build();
각 호출마다도 .advisors(...) 로 추가 가능.
ChatMemory: 대화 이력
@Bean
ChatMemory chatMemory() {
return new InMemoryChatMemory(); // 또는 JdbcChatMemory, RedisChatMemory
}
@Bean
ChatClient chatClient(ChatClient.Builder b, ChatMemory mem) {
return b.defaultAdvisors(new MessageChatMemoryAdvisor(mem)).build();
}
// 사용
String reply1 = client.prompt()
.advisors(spec -> spec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
.user("내 이름은 Alice")
.call().content();
String reply2 = client.prompt()
.advisors(spec -> spec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
.user("내 이름이 뭐였지?")
.call().content();
// → "Alice 입니다."
conversationId 로 세션 분리. 메모리 종류:
InMemoryChatMemory: 프로세스 내, 휘발성JdbcChatMemory: DB 영구CassandraChatMemory,RedisChatMemory: 외부 저장소Neo4jChatMemory: 그래프 기반 (관계 추적 가능)
Image / Audio
@Service
class ImageService {
private final ImageModel imageModel; // OpenAiImageModel (DALL-E 3) 등
public byte[] generate(String description) {
ImageResponse r = imageModel.call(
new ImagePrompt(description,
OpenAiImageOptions.builder()
.withModel("dall-e-3")
.withWidth(1024).withHeight(1024)
.build())
);
return Base64.getDecoder().decode(r.getResult().getOutput().getB64Json());
}
}
Audio: OpenAiAudioTranscriptionModel (whisper), OpenAiAudioSpeechModel (TTS).
Observability
자동으로 Micrometer metric / trace 발행:
spring_ai_chat_client_request_seconds (히스토그램)
spring_ai_chat_client_request_tokens_total
spring_ai_chat_client_response_tokens_total
spring_ai_chat_client_response_finish_reason (counter)
management.tracing.sampling.probability=1.0 으로 trace 활성화 시 prompt / response 도 OTel span 으로 캡처 (민감 데이터 주의).
함정과 베스트 프랙티스
- API key 는 환경 변수:
application.yml에 직접 쓰지 말 것 - temperature / maxTokens 명시: 기본값 의존 시 provider 변경에 취약
- structured output 실패 처리: 잘못된 JSON 응답 시 retry advisor + validation
- tool calling 보안: LLM 이 호출하는 메서드는 부수효과 신중 (DB 쓰기 / 외부 API 등). audit / authorization 필수
- streaming 의 응답 본문 길이 제한: timeout 설정
- provider lock-in 회피: ChatClient 추상화만 사용, provider-specific options 지양
- 테스트는
ChatModelmock: 실제 LLM 호출은 비용
관련 위키
이 글의 용어 (6개)
- [Spring AI] RAG: Embedding, VectorStore, QuestionAnswerAdvisorspring
- 정의 RAG (Retrieval Augmented Generation) 는 LLM 의 지식 한계 (학습 cutoff, 사내 문서 부재) 를 극복하기 위해, 질문 시점에 외부 지식…
- [Spring] 개요: Framework, Boot, Cloudspring
- 정의 Spring은 Java 엔터프라이즈 표준 프레임워크 그룹. 2003년 Rod Johnson이 EJB 대안으로 시작. IoC/DI가 핵심. 현재는: - Spring Frame…
- [Spring] Actuator: health, metrics, infospring
- 정의 Spring Boot Actuator는 프로덕션용 monitoring/management endpoint 자동 제공. health check, metrics, env, be…
- [Spring] Bean Validation: @Valid, @NotBlank, customspring
- 정의 Spring은 Jakarta Bean Validation (JSR-380)을 표준으로 사용. , , 등 어노테이션으로 선언적 검증. Spring MVC가 로 자동 통합. 설…
- [Spring] RestClient, WebClient, RestTemplatespring
- 정의 Spring HTTP 클라이언트 3종: - RestTemplate: 동기, 전통 (5.0+ maintenance) - WebClient: reactive, async/syn…
- [Spring] WebFlux: Reactive, Mono, Fluxspring
- 정의 Spring WebFlux는 Project Reactor 기반의 비동기·non-blocking 웹 프레임워크. Spring MVC와 별개. Netty 기본 (Tomcat도 …
💬 댓글