Spring AI를 활용하여 다양한 생성형 AI 모델과 통신하는 방법을 학습하였다. 하나의 AI 모델만 사용하는 것이 아니라 Groq, Google Gemini, NVIDIA NIM과 같은 여러 LLM(Large Language Model)을 하나의 프로젝트에서 관리하고, 사용자가 원하는 모델을 선택하여 동일한 기능을 수행할 수 있는 구조를 구현하였다.
또한 Spring AI의 ChatClient, ChatModel, Structured Output, System Prompt, ChatOptions 등을 직접 적용하면서 AI 서비스를 객체지향적으로 설계하는 방법을 익힐 수 있었다.
이번 프로젝트에서 가장 먼저 학습한 내용은 ChatClient를 Bean으로 등록하여 AI 모델을 관리하는 방법이었다.
@Bean
public ChatClient geminiChatClient(
@Qualifier("googleGenAiChatModel")
ChatModel chatModel) {
return ChatClient
.builder(chatModel)
.defaultSystem(systemMessage)
.defaultOptions(
GoogleGenAiChatOptions.builder()
.model("gemini-3.1-flash-lite")
.thinkingLevel(
GoogleGenAiThinkingLevel.LOW
)
)
.build();
}
Spring AI에서는 AI 모델과 직접 통신하지 않고 ChatClient를 통해 요청을 전송한다.
ChatClient를 생성할 때
등을 미리 설정할 수 있다.
이렇게 생성된 ChatClient는 Spring Bean으로 등록되기 때문에 여러 Service에서 동일한 설정을 재사용할 수 있다.
즉, 프로젝트 전체에서 AI 모델 설정을 한 곳에서 관리할 수 있다는 장점이 있다.
이번 프로젝트에서는 모든 AI 모델이 공통적으로 사용할 System Prompt를 설정하였다.
private final String systemMessage = """
시작할 때 본인 모델의 정보를 알려주고,
최대한 한글로만 작성,
무엇을 물어보든 식사 메뉴 추천을 함
""";
System Prompt는 사용자의 질문보다 먼저 AI에게 전달되는 지시문이다.
사용자가
오늘 날씨 알려줘
라고 입력하더라도
AI는 먼저
이라는 규칙을 적용한 후 답변을 생성한다.
이를 통해 AI의 답변 스타일을 일관되게 유지하는 방법을 학습하였다.
이번 프로젝트에서는 하나의 AI만 사용하는 것이 아니라 여러 Provider를 동시에 관리하였다.
public enum ModelProvider {
groq,
google,
nim
}
Provider를 Enum으로 관리하면 문자열을 직접 비교하는 것보다 코드의 안정성이 높아지고, 새로운 AI 모델이 추가되더라도 Enum에 값만 추가하면 되므로 확장성이 좋아진다.
또한 Controller에서는 Enum 목록을 화면으로 전달하여 사용자가 원하는 AI 모델을 선택할 수 있도록 구현하였다.
이번 프로젝트에서는 동일한 타입의 Bean이 여러 개 존재하였다.
예를 들어
모두 ChatClient 타입이다.
따라서 Spring은 어떤 Bean을 주입해야 하는지 알 수 없다.
이를 해결하기 위해
@Qualifier("geminiChatClient")
private final ChatClient geminiChatClient;
@Qualifier("nimChatClient")
private final ChatClient nimChatClient;
를 사용하였다.
@Qualifier는 동일한 타입의 Bean이 여러 개 존재할 때 원하는 Bean을 지정하는 역할을 한다.
이를 통해 여러 AI 모델을 하나의 프로젝트에서 동시에 사용할 수 있다는 점을 학습하였다.
AI는 단순히 질문만 보내는 것이 아니라 다양한 옵션을 설정할 수 있다.
.options(
ChatOptions.builder()
.model("llama-3.3-70b-versatile")
.temperature(0.0)
.maxTokens(16)
)
이번 프로젝트에서는 ChatOptions를 이용하여
등을 설정하였다.
Temperature는 AI 답변의 창의성을 의미한다.
값이 낮을수록 일정한 답변을 생성하고,
높을수록 다양한 답변을 생성한다.
MaxTokens는 AI가 생성할 최대 토큰 개수를 제한하여 응답 속도와 비용을 조절하는 역할을 한다.
이번 프로젝트에서는 사용자가 선택한 Provider에 따라 다른 AI를 호출하였다.
switch (dto.provider()) {
case "groq" -> ...
case "google" -> ...
case "nim" -> ...
}
하나의 Service에서 모든 AI를 관리하면서 Provider 값에 따라 적절한 ChatClient를 선택하여 요청을 전송하였다.
이를 통해 동일한 기능을 여러 AI에서 수행하도록 추상화하는 방법을 학습하였다.
이번 프로젝트에서 가장 흥미로웠던 기능은 Structured Output이었다.
return geminiChatClient
.prompt()
.system("...영화 추천...")
.user(dto.message())
.call()
.responseEntity(MovieRecommendationDTO.class);
일반적인 AI는 문자열을 반환한다.
예를 들어
인터스텔라를 추천합니다.
처럼 텍스트만 반환한다.
하지만
.responseEntity(MovieRecommendationDTO.class)
를 사용하면
Spring AI가 AI에게
{
"title":"",
"year":0,
"genre":"",
"reason":""
}
형태의 JSON으로 응답하도록 유도하고,
이를 자동으로
MovieRecommendationDTO
객체로 변환해 준다.
즉,
JSON 파싱을 직접 하지 않아도
AI 응답을 Java 객체로 바로 사용할 수 있다는 점을 학습하였다.
@Builder
public record MovieRecommendationDTO(
String title,
int year,
String genre,
String reason
) {
}
AI가 생성한 JSON 데이터를 DTO와 자동으로 매핑하였다.
이를 통해 문자열을 직접 파싱하지 않고도
를 객체 형태로 사용할 수 있었다.
| 학습 내용 | 세부 학습 내용 |
|---|---|
| Spring AI | ChatClient와 ChatModel을 이용한 AI 서비스 구현 |
| Bean 관리 | AI 모델별 ChatClient를 Bean으로 등록하고 재사용 |
| System Prompt | AI의 역할과 응답 방식을 미리 정의하는 방법 |
| Qualifier | 여러 ChatClient Bean 중 원하는 Bean을 선택하여 주입하는 방법 |
| ChatOptions | 모델, Temperature, Max Tokens 등을 설정하여 AI 응답을 제어하는 방법 |
| Multi Provider | Groq, Gemini, NIM 등 여러 LLM을 하나의 서비스에서 관리하는 방법 |
| Structured Output | AI 응답을 JSON 형태로 생성하고 DTO 객체로 자동 변환하는 방법 |
| DTO 활용 | AI 응답을 객체로 변환하여 서비스와 화면에서 활용하는 방법 |
Spring AI는 단순히 AI에게 질문을 보내는 라이브러리가 아니라, 다양한 LLM을 하나의 애플리케이션에서 일관된 방식으로 사용할 수 있도록 추상화해 주는 프레임워크라는 점을 이해할 수 있었다. 특히 ChatClient를 통한 모델 관리, ChatOptions를 이용한 응답 제어, @Qualifier를 활용한 여러 AI 모델의 의존성 주입, 그리고 Structured Output을 이용해 AI의 응답을 DTO 객체로 자동 변환하는 과정을 직접 구현하면서, 생성형 AI를 기존 Spring 애플리케이션과 자연스럽게 통합하는 방법을 깊이 있게 학습할 수 있었다.