[Dev-Fortune] 시리즈 다시보기


1. 서론: 왜 유료 클라우드 API 대신 로컬 LLM인가?

AI 서비스를 기획할 때 가장 먼저 떠올리는 솔루션은 OpenAI의 ChatGPT API나 Anthropic의 Claude API입니다. 이들 상용 모델은 현존 최고의 성능을 자랑하지만, 개발 단계나 프로토타입 검증 시점에는 치명적인 걸림돌이 따릅니다.

  1. 지속적인 API 과금: 프롬프트 테스트와 RAG 튜닝을 수백 번 반복할 때마다 토큰 비용이 누적됩니다.
  2. 속도 제한(Rate Limit) 및 외부 의존성: 네트워크 레이턴시와 외부 서비스 장애 시 개발 프로세스가 전면 중단됩니다.
  3. 데이터 보안 및 프라이버시: 사내 내부 문서나 사용자의 민감한 개인 정보를 외부 클라우드로 전송할 수 없는 보안 규정(Compliance)에 걸릴 수 있습니다.

이러한 문제를 단번에 해결해 주는 도구가 바로 오픈소스 로컬 LLM 실행기인 Ollama입니다. 이번 2편에서는 Ollama를 로컬 환경에 설치하고, 경량 고성능 모델인 qwen2.5:3b를 서빙한 뒤, Spring Boot 3 프로젝트와 연결하여 최적의 성능을 이끌어내는 설정 기법을 상세히 다룹니다.


2. Ollama 설치 및 로컬 모델 서빙

Ollama는 Go 언어로 작성된 초경량 LLM 런타임으로, C++ 기반의 llama.cpp 엔진을 내장하여 CPU와 GPU(Nvidia CUDA, Apple Metal)를 자동으로 감지하고 최적화해 줍니다.

1) Ollama 설치 및 백그라운드 데몬 실행

리눅스 및 macOS 환경에서는 한 줄의 셸 스크립트로 설치할 수 있습니다.

# 리눅스 / macOS 원클릭 설치
curl -fsSL https://ollama.com/install.sh | sh

# Ollama 데몬 동작 확인 (기본 포트: 11434)
curl http://localhost:11434
# 응답: "Ollama is running"

2) qwen2.5:3b 모델 다운로드 및 터미널 테스트

우리 프로젝트의 두뇌 역할을 할 qwen2.5:3b 모델을 풀(Pull)받고 실행합니다.

# 모델 다운로드 및 실행 (용량 약 1.9GB)
ollama run qwen2.5:3b

# 터미널 프롬프트에서 한국어 질의 테스트
>>> 안녕하세요! 당신은 누구인가요?
# 출력: 안녕하세요! 저는 Qwen입니다. 무엇을 도와드릴까요?

정상적으로 응답이 반환된다면 /bye를 입력하여 대화 모드를 빠져나옵니다.


3. 로컬 연동 아키텍처 다이어그램

Spring Boot 애플리케이션과 Ollama 데몬 간의 네트워크 통신 및 파라미터 전달 구조입니다.

flowchart LR subgraph SpringBoot ["Spring Boot Application"] Client[ChatClient / ChatModel] Config["application.properties"] end
subgraph Host ["Local OS / Server"]
    Config -.->|기본 옵션 주입| Client
    Client == "HTTP POST :11434/api/chat" ==> OllamaDaemon["Ollama Daemon"]
    
    subgraph Hardware ["Hardware Acceleration"]
        OllamaDaemon --> VRAM["VRAM (GPU: CUDA / Metal)"]
        OllamaDaemon --> ModelWeights["Weights: qwen2.5:3b (GGUF Q4_K_M)"]
    end
end

4. Spring Boot 3 의존성 및 프로젝트 설정

1) build.gradle 의존성 추가

Spring AI 1.0.0 M시리즈 또는 마일스톤 버전을 사용하기 위해 spring-ai-bom과 spring-ai-ollama-spring-boot-starter를 등록합니다.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.2'
    id 'io.spring.dependency-management' version '1.1.6'
}

ext {
    set('springAiVersion', "1.0.0-M1")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // Spring AI Ollama 스타터
    implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter'
    
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
    }
}

2) application.properties 상세 설정 및 파라미터 튜닝

Spring Boot의 src/main/resources/application.properties에 Ollama 연결 정보를 정의합니다.

# Ollama 데몬 주소 (기본 로컬 11434 포트)
spring.ai.ollama.base-url=http://localhost:11434

# 사용할 채팅 모델
spring.ai.ollama.chat.options.model=qwen2.5:3b

# 추론 하이퍼파라미터 튜닝
spring.ai.ollama.chat.options.temperature=0.4
spring.ai.ollama.chat.options.num-ctx=4096
spring.ai.ollama.chat.options.top-p=0.9
spring.ai.ollama.chat.options.repeat-penalty=1.15

핵심 파라미터 분석 가이드

  • temperature=0.4: 생성 무작위성을 제어합니다. 값이 1.0에 가까우면 창의적이지만 엉뚱한 답변을 내고, 0에 가까우면 결정론적이고 단조로워집니다. 사주 데이터라는 사실 기반 지식에 시니컬한 말투를 살짝 가미하기 위해 0.4가 가장 균형 잡힌 골든 포인트입니다.
  • num-ctx=4096: 모델이 한 번에 인지할 수 있는 컨텍스트 윈도우 크기(토큰 수)입니다. 기본값(2048)은 RAG 검색 데이터와 시스템 페르소나 지침을 모두 담기에 다소 좁을 수 있어 4096으로 확장합니다.
  • repeat-penalty=1.15: 동일한 문장이나 어미가 반복 출력되는 현상을 억제합니다.

5. ChatClient를 활용한 연동 코드 구현

Spring AI는 현대적인 Fluent API 패턴인 ChatClient를 제공합니다.

1) Configuration 설정

@Configuration
public class AIConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("당신은 실리콘밸리 15년 차 시니컬한 시니어 개발자입니다.")
                .build();
    }
}

2) Controller를 통한 호출 테스트

@RestController
@RequestMapping("/api/test")
@RequiredArgsConstructor
@Slf4j
public class TestOllamaController {

    private final ChatClient chatClient;

    @GetMapping("/ask")
    public String askAI(@RequestParam(defaultValue = "프로덕션 배포 전에 기도해야 할까?") String message) {
        log.info("질문 수신: {}", message);
        
        // 동기식 블로킹 호출 (프로토타입 테스트용)
        String response = chatClient.prompt()
                .user(message)
                .call()
                .content();

        log.info("AI 응답 완료: {}", response);
        return response;
    }
}

브라우저에서 http://localhost:8080/api/test/ask로 접속하면, 몇 초 안에 시니컬한 개발자 톤의 답변이 콘솔과 브라우저에 반환되는 것을 확인할 수 있습니다!


6. 운영 성능 최적화 팁 (Performance Tuning)

  1. GPU 가속 여부 확인: 터미널에서 ollama ps를 실행했을 때 100% GPU로 표시되는지 확인하세요. 만약 100% CPU로 동작 중이라면 Nvidia 드라이버나 CUDA 환경 변수를 점검해야 합니다. GPU 가속 시 응답 속도가 최소 5~10배 빨라집니다.
  2. OLLAMA_KEEP_ALIVE 설정: 기본적으로 Ollama는 5분간 요청이 없으면 메모리에서 모델 가중치를 언로드(Unload)합니다. 다음 요청 때 모델을 디스크에서 다시 읽어오느라 콜드 스타트(Cold Start) 지연이 발생하므로, 상시 서비스 환경에서는 영구 유지하도록 설정합니다.
    export OLLAMA_KEEP_ALIVE=-1
    
  3. 양자화(Quantization) 수준: qwen2.5:3b는 기본적으로 Q4_K_M 양자화가 적용되어 2GB 미만의 VRAM만 소비하므로, 메모리가 부족한 소형 서버에서도 매우 쾌적하게 동작합니다.

7. 결론 및 다음 편 예고

이제 Spring Boot 애플리케이션이 로컬 머신에서 가동 중인 Ollama AI 모델과 원활하게 대화할 수 있는 튼튼한 기반 인프라가 완성되었습니다.

하지만 현재 상태의 모델은 ‘개발자 사주’라는 전문 지식을 전혀 알지 못합니다. 다음 3편에서는 자체 사주 JSON 지식 데이터를 파싱하고, Spring AI의 SimpleVectorStore를 활용해 메모리 기반 RAG 벡터 데이터베이스를 구축하는 데이터 주입(Data Ingestion) 과정을 본격적으로 다루겠습니다.