Spring Boot 4와 Spring AI 2.0을 활용한 로컬 우선(Local-First) AI 어시스턴트 구축하기
요약
Spring Boot 4와 Spring AI 2.0을 활용하여 Python 없이 Java만으로 구축 가능한 로컬 우선(Local-First) AI 플랫폼 Jarvis를 소개합니다. Ollama와 PostgreSQL을 사용하여 데이터 프라이버시를 보호하고 외부 의존성을 최소화한 모듈형 AI 오케스트레이션 아키텍처를 제안합니다.
핵심 포인트
- Java 생태계 기반의 로컬 우선 AI 어시스턴트 구축 방법 제시
- Ollama를 활용한 로컬 AI 실행으로 데이터 프라이버시 및 비용 문제 해결
- Spring AI를 통한 AI 제공업체 간의 교체 가능한 추상화 계층 구현
- JWT 인증, 세션 지속성, 토큰 스트리밍 등 엔터프라이즈급 기능 포함
당신의 AI. 당신의 데이터. 당신의 머신.
지난 몇 년 동안 AI 개발은 Python이 주도해 왔습니다.
개발자들이 AI 프레임워크에 대해 이야기할 때, 대화는 보통 LangChain, LlamaIndex, AutoGPT, CrewAI 및 기타 Python 우선 (Python-first) 생태계를 중심으로 흘러갑니다.
Java 개발자로서 저는 스스로에게 계속 질문했습니다:
Java를 위한 동등한 생태계는 어디에 있는가?
그 답은 이미 존재한다는 것입니다.
Spring AI, Spring Boot 4, WebFlux, PostgreSQL, 그리고 Ollama를 사용하면 이제 완전히 Java만으로 진지한 AI 애플리케이션을 구축하는 것이 가능합니다.
이러한 깨달음은 저로 하여금 Jarvis AI Platform을 구축하게 만들었습니다.
GitHub Repository:
대부분의 AI 어시스턴트가 가진 문제점
대부분의 AI 어시스턴트는 동일한 아키텍처를 따릅니다:
당신의 메시지
↓
클라우드 서비스 (Cloud Service)
...
당신의 대화는 타인의 인프라를 통해 전달됩니다.
당신은 그들의 가동 시간 (Uptime)에 의존합니다.
당신은 그들의 가격 정책에 의존합니다.
당신은 그들의 개인정보 보호 정책 (Privacy policies)에 의존합니다.
만약 내일 서비스가 변경된다면, 당신은 즉각적인 영향을 받습니다.
이 모델은 많은 사람에게 유효합니다.
하지만 저는 다른 것을 원했습니다.
로컬 우선 (Local-First) 대안
Jarvis는 완전히 다른 접근 방식을 따릅니다:
당신의 메시지
↓
당신의 머신 (Your Machine)
...
모든 것이 당신의 컴퓨터에 머뭅니다.
데이터가 당신의 머신을 떠나지 않습니다.
월간 구독료가 없습니다.
핵심 기능을 위한 외부 의존성이 없습니다.
이것이 이 프로젝트의 철학이 단순한 이유입니다:
당신의 AI. 당신의 데이터. 당신의 머신.
Jarvis AI Platform이란 무엇인가?
Jarvis는 단순한 챗봇이 아닙니다.
이것은 Java 생태계를 중심으로 설계된 모듈형 AI 오케스트레이션 (AI orchestration) 플랫폼입니다.
높은 수준 (High level)에서 아키텍처는 다음과 같습니다:
Spring Shell CLI / REST API
│
Spring Boot 4
...
목표는 애플리케이션 아키텍처를 깨끗하고 유지보수 가능하게 유지하면서, AI 제공업체(AI providers)를 서로 교체 가능하게 만드는 것입니다.
v0.1.0의 현재 기능은 다음과 같습니다:
- 토큰 스트리밍 (Token streaming)을 지원하는 대화형 AI 채팅
- JWT 인증 (JWT authentication)
- Argon2id 비밀번호 해싱 (Argon2id password hashing)
- 세션 지속성 (Session persistence)
- PostgreSQL 저장소 (PostgreSQL storage)
- Ollama 로컬 AI 지원 (Ollama local AI support)
- Gemini 폴백 지원 (Gemini fallback support)
- 제공자 추상화 계층 (Provider abstraction layer)
- 작업 기억 시스템 (Working memory system)
- Swagger/OpenAPI 통합 (Swagger/OpenAPI integration)
- 상태 모니터링 및 진단 (Health monitoring and diagnostics)
기술 스택 (Tech Stack)
| 계층 (Layer) | 기술 (Technology) |
|---|---|
| 언어 (Language) | Java 21 |
| ... |
Python 대신 Java를 선택한 이유
제가 자주 듣는 질문 중 하나는 다음과 같습니다:
"왜 이걸 Python으로 만들지 않았나요?"
짧은 답변은 이렇습니다:
저는 Java로 시스템을 구축하는 것을 즐기기 때문입니다.
더 긴 답변을 드리자면, Java는 장기적인 AI 애플리케이션 구축에 있어 몇 가지 이점을 제공합니다:
- 강력한 타입 안정성 (Strong type safety)
- 뛰어난 툴링 (Excellent tooling)
- 성숙한 생태계 (Mature ecosystem)
- 프로덕션 준비가 된 프레임워크 (Production-ready frameworks)
- 리액티브 프로그래밍 지원 (Reactive programming support)
- 엔터프라이즈급 보안 (Enterprise-grade security)
Spring AI는 AI 개발을 Spring 생태계의 자연스러운 확장처럼 느껴지게 만들고 있습니다.
Java 개발자들은 완전히 새로운 스택을 배울 필요 없이, 이미 알고 있는 도구들을 사용할 수 있습니다.
그것이 Jarvis를 만든 가장 큰 동기 중 하나였습니다.
아키텍처 심층 분석 (Architecture Deep Dive)
Jarvis에서 가장 흥미로운 부분은 CLI가 아닙니다.
PostgreSQL도 아닙니다.
AI 모델조차도 아닙니다.
가장 중요한 설계 결정은 사용자(Users)와 AI 제공자(AI providers) 사이에 위치하는 아키텍처였습니다.
첫날부터 목표는 단순했습니다:
Jarvis를 단일 AI 제공자에 종속시키지 말 것.
이 요구사항이 시스템 전체를 형성했습니다.
1. 제공자 추상화 계층 (Provider Abstraction Layer)
Jarvis의 모든 AI 제공자는 동일한 인터페이스를 구현합니다.
public interface AiProvider {
Flux<String> streamChat(Prompt prompt);
...
OllamaProvider와 GeminiProvider 모두 이 계약(Contract)을 구현합니다.
이는 애플리케이션의 나머지 부분이 현재 어떤 제공자가 사용되고 있는지 알 필요가 없음을 의미합니다.
제공자 라우터(Provider router)가 그 책임을 처리합니다.
return ollamaProvider.isAvailable()
.flatMap(ollamaUp -> {
...
이를 통해 제공자에 구애받지 않는(Provider-agnostic) 아키텍처를 구축할 수 있습니다.
Ollama가 실행 중이라면, Jarvis는 Ollama를 사용합니다.
Ollama를 사용할 수 없게 되면, Jarvis는 자동으로 Gemini로 폴백 (Fallback)합니다.
사용자는 아무것도 변경할 필요가 없습니다.
아키텍처는 동일하게 유지됩니다.
새로운 제공자 (Provider)를 추가하는 과정은 매우 간단합니다:
public class ClaudeProvider
implements AiProvider {
}
인터페이스를 구현합니다.
제공자를 등록합니다.
끝입니다.
오케스트레이터 (Orchestrator)를 변경할 필요도 없습니다.
컨트롤러 (Controller)를 변경할 필요도 없습니다.
CLI를 변경할 필요도 없습니다.
2. 리액티브 스트리밍 (Reactive Streaming)
제가 반드시 원했던 기능 중 하나는 실시간 토큰 스트리밍 (Token streaming)이었습니다.
사용자가 전체 응답을 받기 위해 10초 동안 기다리는 것을 원치 않았습니다.
응답이 즉시 나타나기를 원했습니다.
이 요구사항은 프로젝트를 완전한 리액티브 아키텍처 (Reactive architecture)로 이끌었습니다.
흐름은 다음과 같습니다:
Ollama
↓
Spring AI
...
각 토큰은 파이프라인 (Pipeline)을 통해 독립적으로 이동합니다.
사용자는 거의 즉시 출력을 보기 시작합니다.
컨트롤러 엔드포인트 (Controller endpoint)는 다음과 같습니다:
@PostMapping(
value = "/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE
...
결과는 전체 응답을 기다리는 것보다 훨씬 빠르게 느껴집니다.
생성에 몇 초가 걸리더라도, 사용자는 무언가 진행되고 있다는 것을 즉시 알 수 있습니다.
그 작은 개선이 사용자 경험 (User experience)을 극적으로 향상시킵니다.
3. 공백 버그 (The Whitespace Bug)
제가 마주친 가장 이상한 버그 중 하나는 공백과 관련되어 있었습니다.
응답이 다음과 같이 보였습니다:
Hellohowareyoutoday?
다음 대신에 말이죠:
Hello how are you today?
원인은 Server Sent Events (SSE)로 밝혀졌습니다.
토큰 내부의 앞쪽 공백이 전송 중에 손실되고 있었습니다.
해결 방법은 놀라울 정도로 간단했습니다.
가공되지 않은 텍스트 (Raw text)를 보내는 대신, 모든 토큰을 JSON으로 감쌌습니다.
private String jsonToken(String token) {
return "{\"t\":\""
...
그러면 클라이언트가 JSON 페이로드 (Payload)에서 값을 추출합니다.
문제가 해결되었습니다.
때때로 가장 어려운 버그는 AI와 전혀 관련이 없을 때가 있습니다.
그저 공백 때문일 뿐입니다.
4. 작업 기억 (Working Memory)
제가 받는 가장 흔한 질문 중 하나는 다음과 같습니다:
Jarvis는 어떻게 오늘 날짜를 알까요?
답은 간단합니다.
우리가 그 정보를 제공하기 때문입니다.
Jarvis는 모든 요청을 보내기 전에 작은 작업 기억 (Working Memory) 블록을 생성합니다.
@Component
public class WorkingMemoryBuilder {
...
이 메모리는 모든 프롬프트 (Prompt)에 주입됩니다.
AI가 마법처럼 현재 날짜를 알고 있는 것이 아닙니다.
애플리케이션이 단순히 그것을 알려주는 것입니다.
이 차이를 이해하는 것은 현대적인 LLM 애플리케이션이 실제로 어떻게 작동하는지 더 잘 이해하는 데 도움이 되었습니다.
지능적으로 보이는 것들의 상당수는 종종 정교하게 설계된 컨텍스트 (Context)인 경우가 많습니다.
5. 프롬프트 조립 (Prompt Assembly)
모든 사용자 요청은 PromptAssembler라고 불리는 컴포넌트를 통과합니다.
이 컴포넌트의 역할은 최종 프롬프트를 구성하는 것입니다.
조립된 프롬프트는 네 가지 요소를 포함합니다:
- 시스템 지침 (System instructions)
- 작업 기억 (Working memory)
- 세션 기록 (Session history)
- 현재 사용자 메시지 (Current user message)
단순화된 버전:
messages.add(systemPrompt);
messages.add(workingMemory);
...
이 프로세스는 AI가 문맥에 맞는 응답을 생성하는 데 필요한 모든 것을 제공합니다.
프롬프트 조립이 없다면 AI는 현재 메시지만을 보게 될 것입니다.
프롬프트 조립을 통해 AI는 다음을 이해합니다:
- 사용자가 누구인지
- 이전 대화 기록
- 현재 날짜와 시간
- 세션 컨텍스트 (Session context)
- 어시스턴트 지침 (Assistant instructions)
이것이 바로
Spring Security는 리액티브 (Reactive) 애플리케이션에서 다르게 동작합니다.
전통적인 애플리케이션은 ThreadLocal에 크게 의존합니다.
리액티브 애플리케이션은 그럴 수 없습니다.
요청이 여러 스레드에 걸쳐 이동할 수 있기 때문입니다.
대신, WebFlux는 Reactor Context를 사용합니다.
return chain.filter(exchange)
.contextWrite(
ReactiveSecurityContextHolder
...
인증 (Authentication) 정보는 리액티브 스트림 (Reactive stream) 자체와 함께 이동합니다.
이 개념을 이해하고 나니, 많은 WebFlux 보안 패턴들이 갑자기 훨씬 더 이해가 되기 시작했습니다.
Quick Start
Jarvis를 로컬에서 실행하는 데는 몇 분밖에 걸리지 않습니다.
Prerequisites (사전 요구 사항)
- Java 21 이상
- Docker
- Ollama
1. Repository 클론하기
git clone https://github.com/sujankim/jarvis-ai-platform.git
cd jarvis-ai-platform
2. 로컬 모델 다운로드하기
ollama pull llama3.1:8b
이는 약 5GB 정도의 일회성 다운로드입니다.
3. 환경 변수 설정하기
cp .env.example .env
.env 파일을 업데이트하고 보안이 유지되는 JWT 비밀키를 설정하세요.
JARVIS_JWT_SECRET=your-secret-key
4. PostgreSQL 시작하기
docker-compose up -d
5. Jarvis 실행하기
cd server
./mvnw spring-boot:run
Example Session (세션 예시)
jarvis:> login
Username: dravin
...
이 시점에서 모든 것이 사용자의 로컬 머신에서 실행되고 있습니다.
클라우드 의존성은 필요하지 않습니다.
What I Learned (배운 점)
Jarvis를 구축하면서 기대했던 것보다 훨씬 더 많은 것을 배웠습니다.
어떤 교훈은 AI로부터 얻었지만,
대부분은 소프트웨어 엔지니어링 (Software engineering)으로부터 얻었습니다.
리액티브 프로그래밍 (Reactive Programming)은 전통적인 MVC보다 어렵다
그렇지 않은 척할 필요는 없습니다.
전통적인 Spring MVC 애플리케이션이 구축하기 더 쉽습니다.
전통적인 JPA 리포지토리 (Repository)가 이해하기 더 쉽습니다.
블로킹 (Blocking) HTTP 클라이언트가 디버깅하기 더 쉽습니다.
하지만 AI 애플리케이션은 근본적으로 스트리밍 (Streaming) 애플리케이션입니다.
응답을 생성하는 데 종종 몇 초가 걸립니다.
토큰 (Token)을 기다리는 동안 스레드를 블로킹하는 것은 전혀 말이 되지 않습니다.
리액티브 스택을 통해 저는 다음과 같은 것들을 할 수 있었습니다:
- 실시간으로 응답 스트리밍 (Stream responses)
- 여러 대화를 효율적으로 처리
- 스레드 기아 (Thread starvation) 방지
- 진정한 엔드-투-엔드 스트리밍 파이프라인 (End-to-end streaming pipeline) 구축
학습 곡선 (Learning curve)은 가팔랐습니다.
하지만 AI 워크로드 (Workloads)는 전형적인 CRUD 애플리케이션과는 근본적으로 다릅니다.
언어 모델 (Language model)이 응답을 생성하는 데 10~30초가 소요될 때, 스레드를 블로킹 (Blocking)하는 것은 비용이 많이 드는 작업이 됩니다.
리액티브 스트리밍 (Reactive streaming)은 그 문제를 우아하게 해결합니다.
전체 응답이 완료될 때까지 기다리는 대신, 토큰 (Tokens)이 생성되는 즉시 시스템을 통해 흐릅니다.
Ollama
↓
Spring AI
...
그 결과 훨씬 더 반응성이 뛰어난 경험을 제공하게 됩니다.
사용자는 전체 응답을 기다리는 대신 즉시 출력을 받기 시작합니다.
AI 애플리케이션에서 그 차이는 엄청나게 느껴집니다.
그 노력은 충분한 가치가 있었습니다.
Spring AI는 마치 Spring처럼 느껴집니다
Spring AI에서 제가 높게 평가하는 점 중 하나는 이것이 별개의 생태계처럼 느껴지지 않는다는 것입니다.
마치 Spring처럼 느껴집니다.
빌더 (Builders).
의존성 주입 (Dependency injection).
설정 속성 (Configuration properties).
자동 설정 (Auto-configuration).
Java 개발자들이 이미 알고 있는 것과 동일한 관례 (Conventions)들입니다.
Ollama 클라이언트를 만드는 것은 익숙하게 느껴집니다.
Gemini 클라이언트를 만드는 것도 익숙하게 느껴집니다.
제공자 (Providers) 사이를 전환하는 것도 익숙하게 느껴집니다.
이러한 일관성은 마찰을 크게 줄여줍니다.
로컬 AI는 대부분의 사람들이 생각하는 것보다 더 낫습니다
Jarvis를 구축하기 전에는 로컬 모델이 너무 느리거나 너무 제한적일 것이라고 가정했습니다.
제 생각이 틀렸습니다.
llama3.1:8b를 로컬에서 실행하면 놀라울 정도로 유용한 결과가 나옵니다.
다음과 같은 작업에서:
- 일반적인 질문
- 브레인스토밍 (Brainstorming)
- 코딩 보조 (Coding assistance)
- 문서화 도움 (Documentation help)
- 학습 (Learning)
놀라울 정도로 뛰어난 성능을 보여줍니다.
가장 큰 클라우드 모델만큼 유능하냐고요?
아니요.
그럴 필요가 있냐고요?
그것도 아니요.
많은 개인적인 워크플로우 (Workflows)에서 로컬 모델은 이미 충분히 훌륭합니다.
그리고 개인정보 보호 (Privacy) 측면의 이점은 엄청납니다.
모델보다 아키텍처가 더 중요합니다
이것이 아마도 가장 큰 교훈이었을 것입니다.
사람들은 종종 모델에만 완전히 집중합니다.
GPT.
Claude.
Gemini.
Llama.
Mistral.
하지만 실제 AI 애플리케이션은 대부분 아키텍처 (Architecture)의 문제입니다.
프롬프트 관리 (Prompt management).
메모리 (Memory).
보안 (Security).
지속성 (Persistence).
스트리밍 (Streaming).
관측 가능성 (Observability).
프로바이더 라우팅 (Provider routing).
오류 처리 (Error handling).
모델은 시스템의 단 한 부분일 뿐입니다.
Jarvis를 구축하면서 이 아이디어를 반복해서 확인했습니다.
다음 단계는?
Jarvis는 아직 초기 단계입니다.
버전 0.1.0은 기반을 다지는 데 집중합니다.
향후 릴리스에서는 훨씬 더 많은 기능이 추가될 것입니다.
2단계 — 메모리 시스템 (Memory System)
현재 대화는 세션 기반 (Session-based)입니다.
향후 버전에서는 지속성 메모리 (Persistent memory)를 도입할 예정입니다.
계획된 기능은 다음과 같습니다:
- 장기 메모리 (Long-term memory)
- 사용자 선호도 (User preferences)
- Redis 캐싱 (Redis caching)
- 의미론적 검색 (Semantic retrieval)
- pgvector 통합 (pgvector integration)
목표는 간단합니다:
Jarvis는 세션을 넘나들며 유용한 정보를 기억해야 합니다.
3단계 — RAG 엔진 (RAG Engine)
검색 증강 생성 (Retrieval-Augmented Generation, RAG)은 가장 요청이 많은 기능 중 하나입니다.
계획된 기능은 다음과 같습니다:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기