04. 프로젝트 구성
드디어 spring ai의 정식 버전이 2025.05.20일 발표되었다! 그동안 M 버전을 쓰면서 껄끄러웠는데 시원~~
이번 시간에는 Spring AI 기반 프로젝트를 만들고 Model과의 연결을 테스트해보자.
프로젝트 구성
모델 선택
SpringBoot starter를 통해 ai 관련 의존성을 추가할 수 있는데 어떤 모델을 사용할 것인지에 대한 선택지가 제공된다. 이미 Open AI 등의 API Key가 있다면 해당 모델을 선택하면 된다. 이 블로그에서는 Ollama AI를 사용할 계획이다.
Spring AI에서는 BOM(Bill of Materials: 자재 명세서?)을 사용하는데 이는 특정 라이브러리 그룹의 호환되는 버전들을 모아놓은 목록이다. 이를 위해 <dependency> 뿐 아니라 <dependencyManagement> 태그도 추가된다. 따라서 직접 pom.xml을 편집하는 것 보다 starter를 통해서 의존성을 등록하는 편이 좋다.
사실 SpringBoot도 이미 BOM 기반으로 하위 의존성들을 관리하고 있다. boot의 의존성 관리 최 상위 파일인 spring-boot-dependencies.pom을 살펴보면 <dependencyManagement>에서 의존성을 관리하는 코드를 확인할 수 있다.
pom.xml
프로젝트를 위한 의존성으로는 Ollama AI, Spring Web, Spring Boot Dev Tools, Lombok을 추가하자. Open AI는 비교용으로 추가해봤다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.quietjun.springai</groupId>
<artifactId>quietjun_springai</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>quietjun_springai</name>
<description>Demo project for Spring Boot</description>
<url />
<licenses>
<license />
</licenses>
<developers>
<developer />
</developers>
<scm>
<connection />
<developerConnection />
<tag />
<url />
</scm>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.properties
다음으로 사용하는 모델에 적합한 속성을 application.properties에 추가해준다.
ollama는 다음의 자료를 활용하면 된다.
https://docs.spring.io/spring-ai/reference/api/chat/ollama-chat.html
Ollama Chat :: Spring AI Reference
Ollama provides custom Structured Outputs APIs that ensure your model generates responses conforming strictly to your provided JSON Schema. In addition to the existing Spring AI model-agnostic Structured Output Converter, these APIs offer enhanced control
docs.spring.io
다음은 ollama와 관련된 일반적인 설정들이다. ollama.base-url을 제외한 option들은 런타임에도 수정 가능하다. 특히 ollama의 경우 여러가지 모델을 필요에 따라 사용할 수 있어서 런타임에 모델의 이름을 전달하는 것이 일반적이다.
# ollama service url: 기본
spring.ai.ollama.base-url=http://localhost:11434
# 사용하는 model: runtime에 수정 가능
# spring.ai.ollama.chat.options.model=exaone3.5:latest
# 답변을 생성하는 자유도: 기본
spring.ai.ollama.chat.options.temperature=0.1
# context-window 의 개수: 기본
spring.ai.ollama.chat.options.num-ctx=2048
# 모델이 응답에서 생성할 수 있는 토큰의 수
spring.ai.ollama.chat.options.max-tokens=500
# 생성 중 상위 몇 개까지 결과를 채택할 것인가?
spring.ai.ollama.chat.options.top-k=5
위 내용 중 Temperature, Max-Tokens, Top-K에 대한 가이드는 다음과 같다.
- Temperature: 모델 응답의 무작위성 또는 창의성을 제어하는 속성으로 0~1 사이의 double 값을 갖는다.
- Lower Values(0.0 ~ 0.3): 보다 결정적이고 집중적인 응답. 사실적인 질문, 분류 또는 일관성이 중요한 작업에 적합
- Medium Values(0.4 ~ 0.7): 결정론과 창의성 사이의 균형. 일반적인 사용 사례에 적합
- HIgher Values(0.8 ~ 1.0): 보다 창의적이고 다양한 결과로 창의적인 글쓰기, 브레인 스토밍 또는 다양한 옵션 생성에 적합
- Output Length(MaxTokens): 모델이 응답에서 생성할 수 있는 토큰의 수를 제한한다.
- Low Values(5 ~ 25): 단일 단어, 짧은 구문 또는 분류 레이블의 경우
- Medium Values(50 ~ 500): 단락이나 짧은 설명에 적합
- High Values(1000개 이상): 긴 형식의 콘텐츠, 스토리 또는 복잡한 설명에 적합
- 불필요한 장황함 없이 완전한 응답을 얻으려면 적절한 출력 길이를 설정하는 것이 중요하다.
- Sampling Controls(Top-K and Top-P): 생성 중 후보를 선택하는 프로세스를 세밀하게 제어한다.
- Top-K: 고정 개수 방식으로 상위 K 개의 후보만을 고려
- Top-P: 확률 누적 방식으로 확률 합계가 P%에 달할 때까지 후보 선택
- P=0.9이고 A:50%, B:30%, C:15%, D:3%, E:2%라면 A, B, C 선택
만약 OpenAI를 사용한다면 일반적으로 다음과 같은 설정이 가능하다.
# api key
spring.ai.openai.api-key=sk-proj-여러분의-키를-입력하세요
# 사용할 OpenAI chat model의 이름
spring.ai.openai.chat.options.model=gpt-4o-mini
# 창의성을 제어하는데 사용되는 샘플링 온도: 높을 수록 창의성이 높아짐 - 뻘소리할 확율도.. (0~2)
spring.ai.openai.chat.options.temperature=0.8
# 출력과 추론에 사용되는 토큰 수의 상한선
spring.ai.openai.chat.options.maxCompletionTokens=1000
빈 구성 및 테스트
빈 구성
Ollama의 경우 다양한 실제 모델(Exaone, Gemma, ...)들이 존재하기 때문에 runtime에 모델을 지정해서 빈을 구성해야 한다. 하지만 OpenAI의 경우와 같은 상용 서비스들은 표준화된 API 구조를 가지고 있어서 auto-configuration을 통해서 하나의 모델을 빈으로 제공한다.
@Configuration
@RequiredArgsConstructor
@Slf4j
public class AiConfig {
// application.properties의 내용(baseUrl, name, temperature...)을 재정의한다.
private OllamaChatModel getOllamaChatModel(String name, String baseUrl) {
OllamaApi api = OllamaApi.builder().baseUrl(baseUrl).build();
return OllamaChatModel.builder().ollamaApi(api)
.defaultOptions(OllamaOptions.builder().model(name).build())
.build();
}
@Bean
ChatClient ollamaExaoneChatClient(@Value("${spring.ai.ollama.base-url}") String baseUrl) {
return ChatClient.builder(getOllamaChatModel("exaone3.5:latest", baseUrl)).build();
}
@Bean
ChatClient ollamaGemma3ChatClient(@Value("${spring.ai.ollama.base-url}") String baseUrl) {
return ChatClient.builder(getOllamaChatModel("gemma3:4b-it-qat", baseUrl)).build();
}
@Bean
ChatClient ollamaLlama3ToolChatClient(@Value("${spring.ai.ollama.base-url}") String baseUrl) {
return ChatClient.builder(getOllamaChatModel("qwen3:8b", baseUrl)).build();
}
@Bean // 미리 만들어진 ChatModel을 사용하는 경우
ChatClient openAiChatClientDefault(OpenAiChatModel model) {
return ChatClient.builder(model).build();
}
@Bean
ChatClient openAiChatClient() {
OpenAiApi api = OpenAiApi.builder().apiKey("API_KEY").build();
OpenAiChatOptions options = OpenAiChatOptions.builder().model("gpt-4o").build();
OpenAiChatModel model = OpenAiChatModel.builder().openAiApi(api).defaultOptions(options).build();
return ChatClient.builder(model).build();
}
}
빈 구성 테스트
위에서 만들어진 빈들이 잘 동작하는지 테스트해보자.
@SpringBootTest
@Slf4j
class ChatClientGenTest {
@Autowired
@Qualifier("ollamaExaoneChatClient")
ChatClient ollamaExaoneChatClient;
@Autowired
@Qualifier("ollamaGemma3ChatClient")
ChatClient ollamaGemma3ChatClient;
@Autowired
@Qualifier("ollamaQwenChatClient")
ChatClient ollamaQwenChatClient;
@Autowired
@Qualifier("openAiChatClient")
ChatClient openAiChatClient;
@Test
void useChatClient() {
String question = "대한민국의 수도는? 단답형으로 이야기해줘.";
ChatClient[] ccs = { ollamaExaoneChatClient, ollamaGemma3ChatClient, ollamaQwenChatClient, openAiChatClient };
Arrays.stream(ccs).forEach(cc -> {
String result = cc.prompt().user(question).call().content();
log.debug("result: {}", result);
});
}
}
08:46:07 - com.quietjun.springai..checkBean.40 exaone3.5:latest, 서울
08:46:07 - com.quietjun.springai..checkBean.40 gemma3:4b-it-qat, 서울
08:46:14 - com.quietjun.springai..checkBean.40 qwen3:8b, <think>...</think>서울
08:46:14 - com.quietjun.springai..checkBean.40 gpt-4o-mini, 서울입니다.