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

  • Spring AI 첫 실습에 필요한 의존성과 설정은 무엇인가?
  • ChatClient로 첫 응답을 받는 최소 코드는 어떻게 생겼는가?
  • 첫 실습에서 OpenAI와 Ollama를 어떻게 나눠 써야 하는가?
  • 첫 호출 뒤 바로 운영 코드로 가지 않기 위해 무엇을 분리해야 하는가?

개요

첫 실습의 목표는 멋진 챗봇을 만드는 것이 아니다. Spring Boot 애플리케이션에서 모델 호출이 어떤 bean과 설정으로 연결되는지 확인하는 것이다. 첫 호출이 성공하면 그 다음부터는 호출 경계를 어떻게 감쌀지 생각해야 한다.

Spring AI의 ChatClient는 AI model과 통신하는 fluent API다. 동기 호출과 streaming 호출을 모두 지원하지만, 입문 단계에서는 동기 호출로 시작한다.

원리

기본 흐름은 단순하다.

  • Spring AI BOM을 dependency management에 넣는다.
  • provider starter를 추가한다.
  • provider property를 설정한다.
  • ChatClient.Builder를 주입받는다.
  • ChatClient로 prompt를 보내고 응답을 받는다.

이때 provider starter가 model bean과 builder를 자동 구성한다. 따라서 처음에는 코드보다 버전과 property가 더 중요하다.

실습

Maven 예시는 다음처럼 시작한다.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
</dependencies>

OpenAI 설정은 다음처럼 둔다.

spring:
  ai:
    model:
      chat: openai
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        model: gpt-4o-mini
        temperature: 0.2

첫 호출은 service에서 확인한다.

@Service
class FirstChatService {
 
    private final ChatClient chatClient;
 
    FirstChatService(ChatClient.Builder builder) {
        this.chatClient = builder
                .defaultSystem("You answer briefly in Korean.")
                .build();
    }
 
    String ask(String question) {
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}

간단한 Controller를 붙이면 브라우저나 curl로 확인할 수 있다.

@RestController
@RequestMapping("/api/ai")
class FirstChatController {
 
    private final FirstChatService firstChatService;
 
    FirstChatController(FirstChatService firstChatService) {
        this.firstChatService = firstChatService;
    }
 
    @PostMapping("/ask")
    Map<String, String> ask(@RequestBody Map<String, String> body) {
        return Map.of("answer", firstChatService.ask(body.get("question")));
    }
}

성공 경로는 “질문을 보내면 한국어 짧은 답변이 온다”이다.

자주 나는 오류는 API key 누락, model 이름 오타, BOM 버전 불일치, provider quota 초과, network timeout이다. OpenAI GPT-5 계열을 선택하면 temperature가 지원되지 않을 수 있으므로, 해당 모델을 쓸 때는 temperature 설정을 제거하고 공식 문서의 reasoning model option을 확인한다.

운영에서는 이 Controller를 그대로 키우지 않는다. 다음 장에서 AI Gateway로 분리한다.

실전 판단

첫 실습부터 다음 기준을 메모한다.

  • 이 요청은 사용자가 기다릴 수 있는가?
  • 실패하면 어떤 메시지를 보여줄 것인가?
  • prompt 원문을 로그에 남겨도 되는가?
  • 누가 이 기능을 얼마나 호출할 수 있는가?
  • 모델 비용을 feature별로 볼 수 있는가?

입문 단계에서 이 질문을 같이 보아야 나중에 운영 장에서 갑자기 낯설지 않다.

실전 팁

  • 첫 코드는 Controller보다 service에 둔다.
  • OPENAI_API_KEY는 shell history나 노트에 적지 않는다.
  • Ollama로 실습할 때는 먼저 ollama list로 모델명을 확인한다.
  • 응답 원문을 로그에 찍고 싶다면 local profile에서만 제한적으로 찍는다.
  • 첫 성공 뒤에는 streaming보다 timeout, 예외 처리, gateway 분리를 먼저 배운다.

위험 신호!

  • API key를 application.yml에 직접 커밋한다.
  • Controller가 prompt, model, provider 예외를 모두 안다.
  • OpenAI 호출 실패가 사용자에게 stack trace로 노출된다.
  • 첫 데모가 성공했다는 이유로 비용과 quota를 보지 않는다.
  • Ollama 응답 품질을 기준으로 운영 모델 선택을 단정한다.

확인 질문

확인 질문

  • 첫 ChatClient 실습의 목표는 무엇인가?
    • Spring Boot 설정과 bean으로 모델 호출이 연결되는 흐름을 확인하는 것이다.
  • 첫 실습 코드를 Controller에만 두면 왜 위험한가?
    • HTTP 계약과 AI provider 호출 정책이 섞여 테스트와 변경이 어려워지기 때문이다.
  • OpenAI 설정에서 secret은 어디에 둬야 하는가?
    • 환경 변수나 secret manager에 두고 코드와 노트에는 남기지 않는다.
  • 첫 호출 뒤 다음으로 배워야 할 것은 무엇인가?
    • AI Gateway 계층 분리, 예외 변환, timeout, 사용량 기록이다.

학습 연결

참고 문서