Spring

Spring AI로 LLM 연결하기(Feat: ollama)

수수한 인간 2025. 7. 27. 20:30

들어가면서

올해 초 LLM에 관심이 생겨 이것저것 찾아보다가 Spring AI를 알게 되었고, 최근 정식 버전인 1.0.0이 출시되면서 본격적으로 공부하게 되었습니다.
제가 느끼기에 Spring AI는 LLM 연결을 매우 쉽게 도와주며, 다음과 같은 다양한 기능을 제공합니다:

  • 프롬프트 전처리/후처리를 위한 Advisor
  • JSON → DTO 구조화 출력 지원
  • LLM이 메서드를 직접 호출하는 Function Calling
  • RAG, MCP, 멀티모달 등 다양한 고급 기능 지원

덕분에 이제는 Python 기반 도구가 아니더라도, Java 환경에서도 LLM을 효과적으로 활용할 수 있게 되었습니다.
이 블로그 시리즈는 총 3편으로 구성될 예정이며, 각 주제는 다음과 같습니다:

  1. Spring AI로 LLM 연결하고 구조화된 출력하기
  2. Advisor로 LLM에 추가 정보 제공 및 응답 가공하기 (feat. RAG)
  3. Function Calling으로 LLM에게 메서드 제공하기 (feat. AI Agent)
    이번 포스팅은 그 첫 번째 주제인 “Spring AI로 LLM 연결하고 구조화된 출력하기”입니다.

기본적인 설정법

Spring AI는 다양한 LLM 제공자(OpenAI, Azure, Ollama 등)와 연동할 수 있습니다.
저는 이 포스팅에서 로컬에서 실행 가능한 오픈소스 플랫폼인 Ollama를 기준으로 설명하겠습니다.

  • 참고: Spring AI는 Java 17 이상부터 지원됩니다.

Ollama는 다양한 오픈소스 LLM을 PC에 직접 다운로드하여 실행할 수 있는 플랫폼입니다.
따라서 외부로 유출되면 안 되는 개인정보나 대외비 문서를 처리할 때 적합한 선택이 될 수 있습니다.

 - 의존성 추가 (Maven 기준)
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
 - 설정 파일 기본적인 설정
 # Ollama 서버 주소
spring.ai.ollama.base-url=http://localhost:11434 
# 모델 다운로드 전략(always, when_missing, never)
spring.ai.ollama.init.pull-model-strategy=when_missing
# 기본 셋팅 llm모델
spring.ai.ollama.chat.model= qwen3:1.7b 
# 창의성 조절: 높을수록 자유로운 응답
spring.ai.ollama.chat.temperature= 0.8
# 최대 응답 토큰 수
spring.ai.ollama.chat.max-tokens= 1024
# Embedding 모델
spring.ai.ollama.embedding.model=bona/bge-m3-korean

추가적인 설정에 대해서는 url에서 확인 가능합니다.
https://docs.spring.io/spring-ai/reference/api/chat/ollama-chat.html#_chat_properties


예제 코드

Spring AI에서는 LLM의 응답을 두 가지 방식으로 받을 수 있습니다.

  1. 단일 출력 방식: 한 번에 전체 응답을 받아 처리
  2. 스트리밍 출력 방식 (Flux): LLM 응답을 실시간으로 조각 단위로 받기
    예제를 통해 두 방식의 사용법과, JSON 구조화된 출력 방식까지 함께 살펴보겠습니다.

🧠 ChatModel과 ChatClient의 차이

Spring AI에는 ChatModelChatClient라는 두 개의 주요 구성 요소가 있습니다.

  • ChatModel: LLM과의 저수준 통신 역할을 합니다.
  • ChatClient: ChatModel을 래핑한 상위 레벨 API로, 프롬프트 구성, 응답 처리, Advisor, RAG 등 다양한 확장 기능을 제공합니다.

단일 출력 방식

@AllArgsConstructor
@RestController
public class ModelClient {

    // Spring이 OllamaChatModel 구현체 자동 주입
    private final ChatModel chatModel;

    @GetMapping("/v1/model-client")
    public ChatResponse modelClientV1(@RequestParam("text") String text) {

        // 시스템 메시지는 LLM이 반드시 지켜야 할 지침
        // 유저 메시지는 사용자의 입력
        Prompt prompt = new Prompt(
                List.of(
                        new SystemMessage("너는 친절한 AI 비서야. 절대 추론 과정을 보여주지 말고 요약만 응답해."),
                        new UserMessage(text)
                ));

        return ChatClient.create(chatModel)
                .prompt(prompt)
                .call()
                .content(); // 전체 응답을 한 번에 반환
    }
}

스트리밍 출력 방식 (Flux)

스트리밍 방식은 채팅처럼 실시간으로 응답이 필요한 UI에 적합합니다.

@AllArgsConstructor
@RestController
public class ModelClient {

  private final ChatModel chatModel;

  @GetMapping(value = "/v2/model-client", produces = "text/plain;charset=UTF-8")
  public Flux<String> getModelClientV2(@RequestParam("text") String text) {

      // 시스템 메시지는 LLM이 반드시 지켜야 할 지침
      // 유저 메시지는 사용자의 입력
      Prompt prompt = new Prompt(
              List.of(
                      new SystemMessage("너는 친절한 AI 비서야"),
                      new UserMessage(text)
              ));

      return ChatClient.create(this.chatModel)
              .prompt(prompt)
              .stream() // 스트리밍 응답 시작
              .content(); // Flux<String>으로 실시간 조각 응답 수신
    }
}

구조화된 출력 받기 (JSON → DTO)

내부적으로는 LLM에게 "이런 구조로 응답해줘"라고 명시한 후 결과를 DTO에 바인딩합니다.
하지만 LLM이 해당 형식대로 정확하게 응답하지 않으면 파싱 에러가 발생할 수 있습니다.
안정성을 높이기 위해 시스템 메시지에 형식을 반복적으로 명확히 명시하거나, 구조 유도 프롬프트 예제를 함께 제공하는 것이 좋습니다.

@AllArgsConstructor
@RestController
public class ModelClient {

    private final ChatModel chatModel;

    @GetMapping("/v3/model-client")
    public ActorFilms modelClientV3(@RequestParam("text") String text) {

        // 사용자 입력을 이용해 프롬프트 템플릿 구성
        String template = """
            Generate the filmography of 5 movies for {actor}.
        """;

        Prompt prompt = new Prompt(
                List.of(
                        new SystemMessage("너는 친절한 AI 비서야. 절대 추론 과정을 보여주지 말고 요약만 응답해."),
                        new UserMessage(new PromptTemplate(template).render(Map.of("actor", text)))
                ));

        ChatModel gemma = OllamaChatModel.builder()
                .ollamaApi(OllamaApi.builder().build())
                .defaultOptions(
                        OllamaOptions.builder()
                                .model("gemma3n:e4b")
                                .temperature(0.4)
                                .build())
                .build();

        return ChatClient.create(gemma)
                .prompt(prompt)
                .call()
                .entity(ActorFilms.class); // 결과를 DTO로 구조화
    }

    record ActorFilms(String actor, List<String> movies) {}
}

프롬프트 요청 예시 (내부 동작 확인용)
Spring AI가 구조화 출력을 위해 내부적으로 LLM에게 다음과 같이 요청을 구성합니다.

request: ChatClientRequest[
- 저희가 입력한 명령 프롬프트 입력입니다.
prompt=Prompt{
    messages=[SystemMessage{textContent='너는 친절한 AI 비서야. 절대 추론과정을 보여주지 말고 요약만 응답해', messageType=SYSTEM, metadata={messageType=SYSTEM}}, 
    UserMessage{content='Generate the filmography of 5 movies for 하정우.', properties={messageType=USER}, messageType=USER}], modelOptions=org.springframework.ai.ollama.api.OllamaOptions@48e29a2c}, 

- 여기에 해당 DTO 객체의 정의가 들어갑니다.
context={spring.ai.chat.client.output.format=Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "actor" : {
      "type" : "string"
    },
    "movies" : {
      "type" : "array",
      "items" : {
        "type" : "string"
      }
    }
  },
  "additionalProperties" : false
}```
}]

마치며

AI를 실제 프로젝트에 도입하는 것은 어렵고 복잡할 것 같다는 막연한 생각이 있었지만,
Spring AI를 통해 Java 환경에서도 LLM을 손쉽게 연결하고 활용할 수 있다는 가능성을 확인할 수 있었습니다.

물론 성능, 속도, 프롬프트 설계, 적절한 모델 선택 등 실무에서 고려할 요소들은 많지만,
이번 실습을 통해 LLM을 실제 서비스에 적용하는 출발점을 잡을 수 있었습니다.

다음 포스팅에서는 Advisor 기능을 활용해 LLM 응답을 가공하고,
간단한 RAG(Retrieval-Augmented Generation) 기능을 구현해보겠습니다.

이 기능은 Logging, 크롤링된 문서, RDBMS 정보 등 다양한 외부 지식 소스를 LLM과 연결해
더 정확하고 신뢰도 높은 응답을 생성하도록 도와주는 방식입니다.

해당 예시는 github에 있습니다.
url: https://github.com/Zero-Human/springAi

참고자료