Spring AI 핵심 아키텍처 및 ChatClient

케로로·2026년 8월 31일

안녕하세요.
Spring AI에 대한 내용을 정리해보겠습니다. 🙇‍♂️

Spring AI란 무엇인가?

Spring AI는 Java/Spring 개발자가 기존 애플리케이션에 생성형 AI 기능을 쉽고 표준화된 방식으로 통합할 수 있도록 지원하는 AI 애플리케이션 프레임워크입니다. Spring의 핵심 철학인 추상화(Abstraction), 의존성 주입(DI), 자동 설정(Auto-configuration)을 AI 영역으로 확장한 것이 핵심입니다.

생성형 AI 기능: LLM, Embedding Model, Vector Database, RAG, Tool Calling, MCP 등

1. 기존 스프링 개발자가 LLM 도입 시 겪는 문제점

  • API 파편화: OpenAI, Anthropic, Bedrock, Ollama 등 벤더마다 요청/응답 스키마, 인증 방식, 스트리밍 프로토콜이 상이함
  • 모델 종속성: 특정 AI 모델을 사용하다가 변경하려면 애플리케이션 코드를 대폭 수정해야 하는 문제 발생
  • 반복 구현: 재시도(Retry), 타임아웃, SSE 스트리밍 파싱, 토큰 사용량 로깅 등을 프로젝트마다 중복 작성
  • 패턴 부재: RAG, 대화 메모리(Memory), 함수/툴 호출을 위한 표준 인터페이스가 없어 팀마다 제각각 구현
  • 생태계 및 테스트 단절: Spring Security, Actuator, Micrometer 등 기존 엔터프라이즈 기능과 자연스럽게 엮이지 않으며, 모델 호출부 모킹(Mocking) 및 테스트가 번거로움

2. Spring AI의 목적과 설계 철학

  • 표준화된 추상화: Spring Data가 JPA/MongoDB/Redis를 Repository 인터페이스로 추상화했듯이 Spring AI는 다양한 벤더의 LLM을 ChatModel 인터페이스로 추상화하여 설정만 바꾸면 코드를 고치지 않고 모델 교체가 가능함
  • 기업 데이터와 API의 연결: "기업 DATA, 기업 API를 AI 모델에 연결하는 것"을 목표로 하며 기존의 비즈니스 로직, DB, 보안, 관측성 인프라를 AI 영역과 자연스럽게 결합함

3. Spring AI 도입을 고려해야 하는 이유 (장점)

  • 기존 스프링 애플리케이션의 확장: 별도의 Python AI 서비스로 분리할 필요 없이 기존 Java/Spring 코드와 개발 역량, DevOps 체계를 그대로 재사용 가능
  • 엔터프라이즈 기능의 적용:
    • 보안: Spring Security(OAuth2, JWT 등)와 연계하여 AI가 호출하는 내부 API 및 DB 범위를 통제
    • 트랜잭션 및 도메인 결합: AI 호출 결과를 DB에 저장하거나 이벤트 발행, SAGA 패턴 등과 유기적으로 연결
    • 관측성(Observability): Actuator, Micrometer, Prometheus 등을 통해 AI 호출의 Metric, Tracing, Token 사용량을 손쉽게 모니터링
  • AI Agent와의 결합 용이성: 기존의 Service·Repository를 AI Tool로 재사용하고, 사내 데이터(DB·문서)를 벡터 스토어 및 RAG와 결합하여 도메인 지식 활용 극대화

4. Spring AI의 특징

  • AI Model 추상화: OpenAI, Anthropic, Gemini, Ollama 등 다양한 벤더의 LLM이나 Embedding 모델을 일관된 API 구조로 사용할 수 있게 해줍니다. 모델이 바뀌어도 코드를 거의 수정할 필요가 없어 유연합니다.

  • Spring IoC/DI: AI 구성 요소들을 Spring의 Bean으로 등록하고 관리하며 의존성 주입(DI)을 통해 ChatClient나 ChatModel 등을 자유롭게 가져다 쓸 수 있습니다.

  • Auto Configuration: Spring Boot 기반의 자동 설정 기능을 지원하여 복잡한 초기 설정 없이 설정 파일(application.yml)에 API Key 등을 적어두는 것만으로 즉시 AI 기능을 활성화할 수 있습니다.

  • ChatClient: 마치 체인처럼 메서드를 연속해서 호출하는 Fluent API 방식을 제공하여 직관적이고 편리하게 LLM을 호출하고 응답을 받아올 수 있습니다.

  • Advisor: AI 요청을 처리할 때 대화 기억(Memory)이나 RAG 같은 부가 기능을 블록처럼 끼워 넣어 쉽게 확장할 수 있도록 돕는 기능입니다.

  • RAG (검색 증강 생성): 문서(Document), 텍스트 조각화, 임베딩, 벡터 저장소(VectorStore) 등을 추상화하여 사내 데이터나 문서를 AI가 참고하도록 만드는 복잡한 과정을 쉽게 구현할 수 있습니다.

  • Tool Calling: 자바 메서드와 LLM을 서로 연결하여 AI가 상황에 맞게 필요한 기능을 판단하고 실제 액션(데이터 조회, 외부 API 호출 등)을 수행하게 만듭니다.

  • MCP (Model Context Protocol): 외부 Tool이나 Resource 생태계와 표준화된 방식으로 연동할 수 있도록 지원합니다.

  • Spring 생태계 통합: Spring MVC/WebFlux(웹), Spring Security(보안), Spring Data(데이터베이스) 등 기존 Spring 기술들과 자연스럽게 결합됩니다.

  • Enterprise 친화성: 기업용 업무 시스템에 AI 기능을 이질감 없이 녹여내어 대규모 서비스에서도 안정적으로 운영할 수 있는 기반을 제공합니다.

5. Spring 생태계에서의 LLM 애플리케이션 개발 방식

  • AI 제공 업체(OpenAI, Claude, Gemini 등)가 제공하는 REST API와 모델 간의 복잡한 상호작용을 Spring AI가 중재하는 역할을 수행합니다.
  • 개발자는 직접 HTTP 클라이언트를 다루는 대신 Spring AI의 표준화된 구조를 통해 프롬프트를 전송하고 응답을 처리할 수 있습니다.

6. Spring AI의 주요 기능 모듈

  • Models: ChatModel, EmbeddingModel, ImageModel 등을 통해 다양한 AI 모델을 추상화합니다.
  • Prompts: Prompt, Message(System/User/Assistant), PromptTemplate을 제공하여 체계적인 프롬프트 관리가 가능합니다.
  • Structured Output: BeanOutputConverter 등을 활용해 비정형 LLM 응답을 Java 객체로 곧바로 변환할 수 있습니다.
  • RAG 및 관측성: DocumentReader, Splitter, VectorStore, Advisor를 통해 RAG를 쉽게 구현하고, Micrometer를 기반으로 트레이싱 및 메트릭을 수집합니다.

7. Spring AI 기반 Agent와 지능형 아키텍처

Spring AI 기반 Agent

Spring AI 기반 에이전트(Agent)는 사용자의 단순한 질문에 텍스트로 답하는 것을 넘어 복잡한 목표를 스스로 추론(Reasoning)하고 시스템을 조작해 행동(Action)까지 수행하는 '일하는 AI 시스템'을 의미함

예시 시나리오로 보는 에이전트의 동작 방식

  • 사용자 요청: "내일 비 오면 회의실을 실내로 변경해줘"라는 명령을 입력합니다.
  • 추론(Reasoning) 단계:
    • 내일 날씨를 확인해야 하므로 Weather API를 호출해야겠다고 판단합니다.
    • 날씨가 비인지 조건문을 통해 확인합니다.
    • 비가 온다면 Calendar API를 이용해 회의 장소를 변경합니다.
    • 마지막으로 참석자들에게 Email API를 통해 변경 사항을 알립니다.
  • 행동(Action) 단계: 실제로 날씨 API 호출, 회의실 변경 API 호출, 이메일 발송 API를 연쇄적으로 실행한 뒤 "회의 장소를 B 회의실로 변경했습니다"라고 최종 응답합니다.

Spring AI 에이전트의 4가지 핵심 구성 요소

  • LLM (추론): 시스템의 두뇌 역할을 하며 사용자의 요청을 분석하고 다음에 무엇을 해야 할지 순서를 계획합니다.
  • Retrieval (데이터): 필요한 사내 문서나 데이터베이스 정보를 검색하고 조회하여 판단의 근거 자료로 활용합니다.
  • Tools (행동): 날씨 조회, 캘린더 변경, 이메일 발송 등 실제로 비즈니스 로직이나 외부 API를 실행할 수 있는 도구(자바 메서드 등)입니다.
  • Memory (기억): 대화의 맥락이나 이전 작업 상태를 읽고 쓰며 연속성 있는 상호작용을 지원합니다.

AI Agent 기반 애플리케이션 아키텍처

  • 전체 구조: 웹 브라우저, 모바일 앱, 사내 시스템 등 다양한 사용자 환경에서 요청이 들어오면 백엔드 서버(Spring AI가 탑재된 곳)가 중앙에서 이 모든 것을 조율합니다.
  • 핵심 연동 요소:
    • AI 모델: 클라우드나 온프레미스 환경에 있는 대형 언어 모델(Chat Model, Embedding Model)과 통신합니다.
    • 데이터베이스 및 벡터 스토어: 기존 DBMS와 RAG를 위한 Vector Store를 연결해 사내 데이터를 참고합니다.
    • 내부/외부 도구: 자바 코드로 작성된 내부 도구(Tool)와 외부 MCP Server 연동을 통해 시스템이 직접 액션을 취할 수 있는 실행력을 갖추게 됩니다.

ChatClient API

1. ChatModel이란? (저수준 통신 중재자)

  • 개념: OpenAI(GPT), Anthropic(Claude), Google(Gemini) 등 서로 다른 벤더의 LLM과 통신할 때 발생하는 요청·응답 방식의 차이를 일관된 방식으로 다룰 수 있도록 추상화한 핵심 인터페이스입니다.
  • 주요 역할 (입출력 변환):
    • 자바에서 만든 표준 Prompt 객체를 각 AI 모델이 이해하는 Native API 요청 형식(HTTP JSON)으로 변환해 줍니다.
    • 반대로 AI 모델이 돌려준 원시 응답(HTTP Response)을 Spring AI의 표준 응답 객체인 ChatResponse로 깔끔하게 변환해 줍니다.
  • 특징: 개발자가 어떤 AI 모델을 쓰든 동일한 코드로 상호작용할 수 있게 만들어 주지만, 직접 다루기에는 코드가 다소 복잡할 수 있어 주로 프레임워크 내부 확장이나 커스터마이징에 적합한 저수준(Low-level) API입니다.
// 프롬프트를 직접 조립하고 응답을 파싱해야 함
Prompt prompt = new Prompt(new UserMessage("오늘 날씨 어때?"));
ChatResponse response = chatModel.call(prompt);
String result = response.getResult().getOutput().getContent();

2. ChatClient란? (고수준 개발자 친화적 도구)

  • 개념: ChatModel 위에서 Prompt, Advisor, Tool, 응답 처리 등을 개발자가 가장 편하게 조작할 수 있도록 만든 고수준(High-level)의 팩사드(Façade) API입니다.
  • 작동 방식 (Fluent API): 메서드를 줄줄이 연결하는 메서드 체이닝(Method Chaining) 방식을 사용하여 가독성이 높고 직관적입니다.
  • 실제 코드 흐름 예시 (/ai 요청 처리):
    • chatClient.prompt(): 대화를 시작합니다.
    • .user(userInput): 사용자의 입력(질문)을 전달합니다.
    • .call(): 백그라운드에서 LLM AI를 호출합니다.
    • .content(): 결과 중에서 순수 텍스트 내용(String)만 깔끔하게 추출합니다.
// 가독성이 높고 직관적인 체이닝(Chaining) 방식 사용
String result = chatClient
  .prompt()
  .user("Spring AI란 무엇인가?")
  .call()
  .content();
구분ChatModelChatClient
추상화 수준저수준 (Low-level API)고수준 (High-level Fluent API)
주요 용도AI 모델과의 기본 통신 및 데이터 규격 변환애플리케이션 개발 시 프롬프트 작성, 옵션 설정, 부가 기능(Advisor/Tool) 조합
코드 스타일객체를 직접 생성하고 응답 파싱 과정이 다소 길음 (new Prompt(...) → chatModel.call(...) → getResult())메서드 체이닝으로 간결하고 직관적 (chatClient.prompt().user(...).call().content())

3. Prompt 클래스와 Message 클래스

Prompt 클래스

  • 개념: LLM에 전달할 대화 목록(messages)과 모델 실행 옵션(options)을 함께 담아 ChatModel에 전달하는 최종 요청 객체입니다.
  • 구성 요소:
    • messages: LLM과 주고받을 대화 메시지들의 리스트(List<Message>).
    • options: 모델 이름, 창의성 조절을 위한 temperature, 최대 토큰 수(maxTokens) 등 모델 호출 시 필요한 설정값(ChatOptions).
// ChatModel에 전달하는 모델 요청 객체
public final class Prompt implements ModelRequest<List<Message>> {
  // LLM에 전달할 Message 목록
  private final List<Message> messages;
  // 모델 호출 옵션 (model, temperature, maxTokens 등)
  private final ChatOptions options;
}

Message 클래스

  • 개념: LLM과 주고받는 대화 정보를 역할(role)별로 명확하게 표현하기 위해 자바의 봉인된 인터페이스(sealed interface Message)로 정의된 구조입니다.
  • 4가지 핵심 역할별 메시지 유형:
    • SystemMessage
      • LLM이 수행해야 할 역할, 지침, 규칙 등을 미리 정의해 주는 시스템 지시사항입니다.
      • 예시: "너는 친절하고 전문적인 고객 센터 상담원이야. 답변은 항상 3줄 이내의 존댓말로 간결하게 작성해줘."
    • UserMessage
      • 사용자가 입력한 요청 사항이며, 텍스트뿐만 아니라 이미지나 오디오 같은 멀티미디어(Media) 데이터도 함께 포함할 수 있습니다.
      • 예시: "이번에 구매한 스마트폰 영수증인데 보증 기간이 언제까지인지 사진 보고 확인해줘." (텍스트 + 영수증 이미지 데이터)
    • AssistantMessage
      • 사용자의 질문에 대한 LLM(AI)의 응답이며 필요시 내부 기능을 실행하기 위한 ToolCall 정보를 함께 포함할 수 있습니다.
      • 예시: "고객님, 영수증 확인 결과 보증 기간은 2027년 8월까지입니다. 추가로 남은 AS 센터 위치를 조회해 드릴까요?"
    • ToolResponseMessage
      • AI가 요청한 Tool(자바 메서드 등)을 실제로 실행한 뒤, 그 결과를 다시 LLM에게 돌려줄 때 사용하는 메시지입니다.
      • 예시: {"location": "분당센터", "distance": "1.2km"} (AI가 요청한 주변 AS 센터 조회 자바 메서드의 실행 결과 데이터를 다시 AI에게 전달)

4. ChatClient를 활용한 동기/비동기 대화

1. 동기 방식
의미: 웹 브라우저나 클라이언트가 요청을 보냈을 때, AI가 답변을 다 완성할 때까지 기다렸다가 한 번에 통째로 텍스트 응답을 받는 방식입니다.

@GetMapping("/ai")
public String generation(@RequestParam String userInput) {
  //The Fluent API in Action
  return this.chatClient.prompt()
  .user(userInput)
  .call()
  .content()
}

코드 흐름:

  • chatClient.prompt(): 대화 요청을 시작합니다.
  • .user(userInput): 사용자가 입력한 질문(문자열)을 담습니다.
  • .call(): 백그라운드에서 AI 모델을 동기 방식으로 호출합니다.
  • .content(): 결과 응답 중 순수 텍스트 내용(String)만 추출하여 반환합니다.

2. 비동기/스트리밍 방식
의미: ChatGPT처럼 AI가 답변을 한 글자씩 실시간으로 줄줄이 만들어 내는(타이핑 효과) 스트리밍 응답 방식입니다. 스프링의 리액티브(Reactive) 기술인 Flux<String>을 사용합니다.

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

코드 흐름:

  • 반환 타입이 일반 String이 아니라 Flux<String>이며, 미디어 타입도 TEXT_EVENT_STREAM_VALUE(SSE)를 사용합니다.
  • .stream(): 동기 방식의 .call() 대신 스트리밍 호출 방식을 지정합니다.
  • .content(): 토큰(단어 조각) 단위로 생성되는 텍스트를 실시간 연속 데이터 흐름(Flux)으로 내려보냅니다.

5. ChatClient.Builder

  • 개념: ChatClient를 만들 때 기본 시스템 지시사항(System Prompt), 기억 장치(Advisor), 외부 도구(Tool) 등의 기본 설정을 미리 조립해 두고 재사용 가능한 맞춤형 ChatClient를 찍어내는 공장(Builder) 역할입니다.
  • 활용 사례 2가지:
    • 기본 설정 적용: "당신은 사내 기술지원 AI Agent입니다"라는 기본 시스템 프롬프트와, 대화 기억(ChatMemoryAdvisor), 사내 문서 검색(QuestionAnswerAdvisor) 같은 부가 기능을 미리 장착한 ChatClient를 생성합니다.
    • Multi-Model 환경 설정: 스프링의 @Bean 설정을 통해, OpenAI를 바라보는 chatClient와 Anthropic(Claude)을 바라보는 chatClient를 각각 따로 만들어 하나의 프로젝트 안에서 여러 모델을 동시에 골라 쓸 수 있도록 구성합니다.
// 기본 설정이 적용된 ChatClient 생성 코드
@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder, 
                                 ChatMemory chatMemory, 
                                 VectorStore vectorStore) {
        return chatClientBuilder
            .defaultSystem("당신은 사내 기술지원 AI Agent입니다.")
            .defaultAdvisors(
                // 대화 기억(Memory) 기능을 기본 장착
                MessageChatMemoryAdvisor.builder(chatMemory).build(),
                // 사내 문서 검색(RAG)을 위한 벡터 스토어 어드바이저 기본 장착
                QuestionAnswerAdvisor.builder(vectorStore).build()
            )
            .build();
    }
}
// Multi-Model 환경 설정 코드 (OpenAI 및 Claude 분리)
@Configuration
public class MultiModelConfig {

    // OpenAI 전용 ChatClient 설정
    @Bean
    public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
        return ChatClient.builder(openAiChatModel)
            .defaultSystem("당신은 OpenAI 기반 AI입니다.")
            .build();
    }

    // Anthropic(Claude) 전용 ChatClient 설정
    @Bean
    public ChatClient claudeChatClient(AnthropicChatModel anthropicChatModel) {
        return ChatClient.builder(anthropicChatModel)
            .defaultSystem("당신은 Claude 기반 AI입니다.")
            .build();
    }
}

0개의 댓글