이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • ChatClient, ChatModel, auto-configuration의 책임은 어떻게 다른가?
  • Spring AI 2.0 기준 OpenAI 설정과 retry 설정은 어떻게 잡는가?
  • 기능별 ChatClient와 model profile을 왜 분리하는가?
  • provider 옵션을 운영에서 어떻게 관리해야 하는가?

개요

ChatClient는 Spring AI에서 가장 자주 만나는 API다. 하지만 ChatClient는 모델 자체가 아니라 모델 호출을 애플리케이션 친화적으로 감싼 API다. 실제 provider 호출은 ChatModel 계층이 담당한다.

auto-configuration은 시작을 쉽게 해주지만 운영 정책을 대신 정해주지는 않는다. model profile, retry, timeout, fallback, usage record는 애플리케이션 요구에 맞춰 설계해야 한다.

원리

기본 흐름은 다음과 같다.

  • provider starter가 classpath에 들어온다.
  • application.yml의 provider property를 읽는다.
  • Spring Boot auto-configuration이 provider별 model bean을 만든다.
  • ChatClient.Builder가 구성된다.
  • 애플리케이션은 기능별 ChatClient를 만든다.

같은 ChatModel을 쓰더라도 기능별 ChatClient는 system prompt, advisor, tool, output format이 다를 수 있다.

실습

OpenAI 기본 설정은 Spring AI 2.0 property 형태를 따른다.

spring:
  ai:
    model:
      chat: openai
    retry:
      max-attempts: 3
      backoff:
        initial-interval: 1s
        multiplier: 2
        max-interval: 5s
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        model: gpt-4o-mini
        temperature: 0.2

기능별 client를 만든다.

@Configuration
class AiClientConfig {
 
    @Bean
    ChatClient supportChatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("""
                        You are a customer support assistant.
                        Answer in Korean.
                        Do not invent product policies.
                        """)
                .build();
    }
 
    @Bean
    ChatClient ticketClassifierChatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("Classify support tickets into stable categories.")
                .build();
    }
}

model profile은 기능 코드와 물리 모델명을 분리한다.

record ModelProfile(
        String name,
        String provider,
        String model,
        double temperature,
        int maxOutputTokens
) {
}
 
@Service
class ModelProfileResolver {
 
    private final Map<String, ModelProfile> profiles = Map.of(
            "default-chat", new ModelProfile("default-chat", "openai", "gpt-4o-mini", 0.2, 800),
            "strict-json", new ModelProfile("strict-json", "openai", "gpt-4o", 0.0, 400)
    );
 
    ModelProfile resolve(String name) {
        ModelProfile profile = profiles.get(name);
        if (profile == null) {
            throw new IllegalArgumentException("Unknown model profile: " + name);
        }
        return profile;
    }
}

성공 경로는 기능 코드가 gpt-4o-mini 같은 물리 모델명을 직접 알지 않는 것이다.

자주 나는 오류는 retry를 너무 크게 잡는 것이다. provider 429 상황에서 retry가 비용과 지연을 더 키울 수 있다.

또 다른 오류는 모델 family별 option 차이를 무시하는 것이다. Spring AI OpenAI 문서는 GPT-5 계열 모델이 temperature를 지원하지 않는다고 설명한다. 이 장의 예시는 temperature를 보여주기 위해 non-reasoning 모델을 사용한다. GPT-5 계열을 model profile에 넣는다면 temperature를 빼고 reasoning model에 맞는 token option을 확인한다.

운영에서는 model profile 변경을 feature flag나 config release로 관리하고 eval regression을 통과시킨다.

실전 판단

ChatClient를 하나만 둘지 여러 개 둘지는 기능 책임으로 판단한다. prompt, advisor, output type, tool set이 다르면 다른 client로 보는 편이 낫다.

retry는 “실패를 줄이는 장치”가 아니라 “실패를 비용과 지연으로 바꾸는 장치”일 수 있다. 429, timeout, 5xx 같은 일시 장애에만 제한적으로 적용한다.

실전 팁

  • provider별 property는 공식 문서 기준으로 확인한다.
  • 기능별 client 이름을 명확히 둔다.
  • model profile에 model, temperature, max token, provider를 함께 둔다.
  • retry max attempts는 운영 초기에 작게 시작한다.
  • provider raw option을 내부 도메인 service까지 끌고 가지 않는다.

위험 신호!

  • OpenAI chat 설정에 예전 options segment를 최신 문서 확인 없이 사용한다.
  • 기능 코드마다 model name을 직접 쓴다.
  • retry 기본값을 확인하지 않고 운영 트래픽에 올린다.
  • temperature=0을 deterministic test의 근거로 삼는다.
  • provider별 옵션 차이를 무리하게 완전히 공통화하려 한다.

확인 질문

확인 질문

  • ChatClientChatModel의 차이는 무엇인가?
    • ChatClient는 애플리케이션 API이고, ChatModel은 provider 모델 호출 추상화다.
  • 기능별 ChatClient를 나누는 기준은 무엇인가?
    • system prompt, advisor, tool set, output format, timeout 정책이 다르면 나눈다.
  • model profile을 두는 이유는 무엇인가?
    • 기능 코드가 물리 모델명과 provider option에 직접 묶이지 않게 하기 위해서다.
  • retry 설정에서 조심할 점은 무엇인가?
    • retry가 비용, latency, rate limit 문제를 증폭할 수 있다는 점이다.

학습 연결

참고 문서