프로젝트 · 2026년 이전2026.05 (v1)

PrivateAI

Spring Backend의 업무 데이터와 사용자 PC의 Local LLM을 Local Agent로 연결한 AI 작업 처리 서비스

JavaSpring BootSpring Data JPASpring SecurityJWTReactTypeScriptNode.jsAxiosOllamaLlama 3.2RDBMS
담당 역할개인 프로젝트 · 기획, 설계 및 전체 개발
처리 규모
성능 개선

01 / 프로젝트 개요

PrivateAI는 서버의 업무 데이터와 사용자 로컬 환경의 LLM을 연결하여 AI 작업을 처리하는 서비스입니다.

Spring Boot Backend에서 사용자와 Project, Task, Action 등의 서비스 데이터 및 AI 작업을 관리합니다. 사용자 PC에서 실행되는 Node.js Local Agent는 대기 중인 AI 작업을 가져가 Ollama의 Llama 3.2로 처리한 뒤 결과를 서버에 반환합니다.

이를 통해 서버가 사용자의 Local LLM에 직접 접근하지 않아도 AI 기능을 제공할 수 있는 Backend ↔ Local Agent ↔ Local LLM 구조를 구현했습니다.

02 / 해결하려던 문제

Local LLM은 사용자 PC 내부에서 실행되기 때문에 외부 서버가 사용자의 Ollama에 직접 접근하기 어렵습니다.

AI 요청을 단순한 동기 API 호출로 처리하는 대신, 서버가 AI 작업을 AiJob으로 관리하고 Local Agent가 서버에 먼저 접근하여 작업을 가져가는 Polling 방식을 적용했습니다.

이를 통해 서비스의 데이터와 작업 상태는 서버에서 관리하면서 실제 LLM 추론은 사용자 로컬 환경에서 수행할 수 있도록 역할을 분리했습니다.

03 / 주요 기능

FEATURE 01

프로젝트 및 작업 관리

Project를 기준으로 Task와 Action을 관리하고 ActionLog를 통해 작업 데이터를 기록할 수 있도록 구성했습니다.

사용자가 자신의 프로젝트와 작업 데이터만 조회·변경할 수 있도록 인증 정보와 리소스 소유권을 함께 검증했습니다.

FEATURE 02

AI 작업 생성 및 비동기 처리

AI 요청을 즉시 LLM에 전달하지 않고 AiJob으로 생성합니다.

각 작업은 PENDING → PROCESSING → COMPLETED / FAILED 상태를 가지며 서버에서 진행 상태와 결과를 관리합니다. 사용자는 AI 작업 생성 후 Job 조회 API를 통해 처리 상태와 결과를 확인할 수 있습니다.

FEATURE 03

Local Agent 기반 Local LLM 연동

사용자 PC에서 Node.js Local Agent를 실행하고, Agent가 Spring Backend를 주기적으로 Polling하여 처리할 PENDING 작업을 조회하도록 구현했습니다.

작업을 가져온 Agent는 Ollama의 Llama 3.2에 요청을 전달하고 처리 결과 또는 실패 정보를 다시 Spring Backend에 반환합니다. 이를 통해 서버가 사용자 PC의 Local LLM에 직접 접근할 필요가 없는 구조를 구성했습니다.

04 / 시스템 구조

등록된 아키텍처 이미지가 없습니다.
구조 설명

PrivateAI v1은 크게 Spring Backend, Node.js Local Agent, Ollama 세 영역으로 구성됩니다.

Client → Spring Backend → AiJob 저장
사용자가 AI 기능을 요청하면 Backend가 요청 대상과 작업 정보를 AiJob으로 생성하고 PENDING 상태로 저장합니다.

Node Local Agent → Spring Backend
사용자 PC에서 실행되는 Local Agent가 Backend를 Polling하여 처리할 작업을 조회하고 가져옵니다.

Local Agent → Ollama / Llama 3.2
Agent가 작업에 필요한 데이터를 기반으로 Local LLM을 호출하여 AI 작업을 수행합니다.

Ollama → Local Agent → Spring Backend
처리가 완료되면 Agent가 결과를 Backend에 반환하고 Backend는 해당 Job을 COMPLETED로 변경합니다. 처리 중 문제가 발생한 경우에는 FAILED 상태로 관리합니다.

Backend는 서비스 데이터와 AI 작업 상태를 관리하고, Local Agent는 서버와 로컬 AI 환경을 연결하며, Ollama는 실제 추론을 담당하도록 책임을 분리했습니다.

05 / 주요 기술적 결정

기술 결정 01

서버가 Local LLM을 직접 호출하지 않는 구조

고민
일반적인 외부 AI API처럼 Spring Backend에서 LLM을 직접 호출하는 구조를 고려할 수 있었지만, Ollama는 사용자 PC에서 실행되므로 외부 서버가 각 사용자의 Local LLM Endpoint에 직접 접근하기 어렵습니다.

결정
사용자 환경에 Node.js Local Agent를 두고 Agent가 서버에 먼저 연결하도록 구성했습니다. Spring Backend는 AI 요청을 AiJob으로 저장하고 Local Agent가 Polling으로 작업을 가져갑니다.

결정 이유
로컬 환경에서 서버 방향으로 통신을 시작하면 사용자 PC의 Local LLM을 외부에 직접 노출하지 않고도 AI 작업을 처리할 수 있습니다. AI 작업의 생성과 상태는 서버가 관리하고 실제 추론만 Local Agent가 담당하도록 책임도 명확하게 분리할 수 있었습니다.

기술 결정 02

AI 요청을 비동기 Job으로 관리

고민
Local LLM 추론은 일반적인 CRUD 요청보다 처리 시간이 길고 Local Agent의 실행 상태에도 영향을 받습니다. HTTP 요청 하나를 LLM 처리 완료 시점까지 유지하는 동기 방식은 서버와 Local Agent가 분리된 구조에 적합하지 않았습니다.

결정
AI 요청 자체를 AiJob이라는 독립적인 리소스로 관리하고 PENDING → PROCESSING → COMPLETED / FAILED 상태를 통해 처리 과정을 관리하도록 설계했습니다.

결정 이유
AI 요청 생성과 실제 LLM 처리를 분리하여 Local Agent가 즉시 작업을 수행하지 못하더라도 요청 정보를 유지할 수 있고, 처리 성공과 실패 역시 서버에서 일관된 상태로 관리할 수 있기 때문입니다.

기술 결정 03

별도 메시지 브로커 대신 Polling 적용

고민
Backend와 Local Agent 사이의 작업 전달에 메시지 브로커를 도입할 수도 있지만, 개인 프로젝트 규모에서 별도의 메시징 인프라를 추가하면 운영 복잡도가 크게 증가합니다.

결정
기존 Spring REST API를 활용하여 Local Agent가 일정 주기로 대기 작업을 조회하는 Polling 방식을 적용했습니다.

결정 이유
현재 프로젝트에서 필요한 것은 대규모 작업 처리량보다 Backend와 사용자 Local Runtime 사이의 안정적인 작업 전달 구조를 직접 구현하고 검증하는 것이었습니다. 별도의 메시징 인프라 없이 구현 가능한 Polling을 선택해 시스템 복잡도를 낮췄습니다.

06 / 트러블슈팅

트러블슈팅 사례 01

Polling에서 '작업 없음'을 예외로 처리한 문제

Polling API에서는 '데이터 없음' 자체가 정상적인 상태가 될 수 있다는 점을 확인했습니다. API의 성공 여부를 단순히 데이터 존재 여부로 판단하기보다 해당 API가 사용되는 흐름과 상태의 의미를 기준으로 응답을 설계해야 한다는 것을 배웠습니다.

트러블슈팅 사례 02

AiJob 생성 시 상태값 누락으로 인한 DB 저장 실패

DB의 NOT NULL 제약조건만으로는 애플리케이션의 상태 규칙을 충분히 표현할 수 없으며, 도메인 객체가 생성되는 순간부터 유효한 상태를 갖도록 보장하는 것이 중요하다는 점을 확인했습니다. 특히 비동기 작업처럼 상태 변화가 핵심인 도메인에서는 초기 상태와 전이 규칙 자체가 중요한 비즈니스 규칙이라는 것을 경험했습니다.