Stack AI 0원: 상세 설명과 예제 포함

이 글에는 짧은 영상이 있습니다:YouTubeTikTok

1. 개요

많은 사람들이 AI 시스템을 구축하면(에이전트, RAG, 내부 챗봇) 매달 수천 달러가 들 것으로 생각합니다. OpenAI나 Anthropic 같은 곳의 API 비용을 내야 하기 때문입니다. 하지만 실제로는 다릅니다. 8개 계층으로 이루어진 스택이 있는데, 각 계층마다 무료 또는 셀프 호스팅 옵션이 있어서, 0원으로 완전한 AI 시스템을 구축하고 운영할 수 있습니다. 이 글은 스택의 각 계층을 설명하고, 실제 코드 예제를 통해 어떻게 연결하는지 보여줍니다. 읽고 나면 각 계층에서 어떤 도구를 선택해야 하는지, “무료”의 실제 한계가 어디에 있는지 알 수 있을 것입니다.

2. “0원”이 영구적으로 무료라는 뜻은 아닙니다

세부사항으로 들어가기 전에 명확히 해야 할 한 가지가 있습니다. 여기서 말하는 “0원”은 소프트웨어/라이선스 비용이지, 인프라 비용 전체가 없다는 뜻은 아닙니다. Vercel 무료 계층은 대역폭 제한이 있고, Supabase 무료 계층도 스토리지 제한이 있으며, 로컬 Ollama를 실행하면 당신의 컴퓨터 CPU/RAM을 사용합니다. 이 스택은 프로토타입, 학습, 소규모 고객 데모에 적합하지만, 수백만 사용자를 위해 확장하려는 용도로는 맞지 않습니다.

참고: 실제 트래픽이 무료 계층을 초과하면 여전히 비용을 내야 합니다. 다만 LLM 공급업체에 토큰 수로 비용을 내는 대신 인프라(컴퓨트, 스토리지)에 비용을 냅니다. 실제 프로덕션을 위해 이 스택을 고수하기 전에 이 차이를 이해하는 것이 중요합니다.

3. 프론트엔드 계층: Next.js 또는 Streamlit

프론트엔드 계층은 복잡할 필요가 없습니다. 아름다운 UI를 원하면 Next.js를 사용하여 Vercel 무료 계층에 배포하세요. AI 파이프라인의 빠른 데모만 필요하면 Streamlit이 훨씬 더 빠른 선택입니다. 별도의 프론트엔드를 작성할 필요가 없기 때문입니다.

# app.py
import streamlit as st
import requests

st.title("내부 문서 질답 보조원")

question = st.text_input("질문을 입력하세요")

if st.button("물어보기") and question:
    response = requests.post(
        "http://localhost:8000/ask",
        json={"question": question},
        timeout=30,
    )
    st.write(response.json()["answer"])

이 코드는 입력 필드와 버튼을 만듭니다. 버튼을 클릭하면 백엔드 API(로컬에서 실행 중)를 호출하고 답변을 표시합니다. streamlit run app.py로 실행하면 Streamlit이 localhost:8501에서 자동으로 호스팅됩니다. React를 작성할 필요가 없고, 빌드 파이프라인도 필요 없습니다. Next.js에 투자하기 전에 아이디어를 검증하는 단계에 적합합니다.

4. 에이전트 오케스트레이션 계층: LangGraph

시스템이 단순히 ’LLM을 한 번 호출하고 결과를 반환’하는 것을 넘어 여러 단계(검색 → 종합 → 검증 → 답변)가 필요하면 오케스트레이터가 필요합니다. LangGraph는 에이전트를 상태를 가진 그래프로 모델링하여 분기와 반복을 허용합니다.

<!-- 여기서는 pom.xml이 적용되지 않습니다. LangGraph는 Python 라이브러리이기 때문입니다. -->
pip install langgraph langchain-ollama
from langgraph.graph import StateGraph, END
from typing import TypedDict

class AgentState(TypedDict):
    question: str
    answer: str

def retrieve_step(state: AgentState) -> AgentState:
    state["answer"] = f"다음 질문에 대한 문서를 찾는 중입니다: {state['question']}"
    return state

def generate_step(state: AgentState) -> AgentState:
    state["answer"] += " -> 답변을 종합했습니다"
    return state

graph = StateGraph(AgentState)
graph.add_node("retrieve", retrieve_step)
graph.add_node("generate", generate_step)
graph.set_entry_point("retrieve")
graph.add_edge("retrieve", "generate")
graph.add_edge("generate", END)

app = graph.compile()
result = app.invoke({"question": "보증 정책이 무엇인가요?", "answer": ""})
print(result["answer"])
다음 질문에 대한 문서를 찾는 중입니다: 보증 정책이 무엇인가요? -> 답변을 종합했습니다

StateGraph는 두 개의 노드(retrieve와 generate)로 구성된 처리 흐름을 정의합니다. 각 노드는 상태를 받아서 새로운 상태를 반환합니다. set_entry_point는 시작 노드를 지정하고, add_edge는 단계들을 연결합니다. app.invoke(...)를 호출하면 LangGraph가 그래프를 순차적으로 실행하고 최종 상태를 반환합니다. 모든 것이 로컬에서 실행되며, 비용이 드는 API를 호출하지 않습니다.

5. RAG 파이프라인 계층: Chroma와 Qdrant

RAG(Retrieval-Augmented Generation)는 임베딩을 저장하고 유사도로 검색하기 위한 벡터 스토어가 필요합니다. Chroma는 임베디드로 실행되어 별도의 서버가 필요 없습니다. 프로토타입에 적합합니다.

pip install chromadb
import chromadb

client = chromadb.PersistentClient(path="./chroma_data")
collection = client.get_or_create_collection(name="policies")

collection.add(
    documents=["상품은 구매 날짜부터 12개월 동안 보증됩니다.",
               "상자가 원래 상태로 남아있으면 7일 이내에 반품할 수 있습니다."],
    ids=["doc1", "doc2"],
)

results = collection.query(query_texts=["보증 정책"], n_results=1)
print(results["documents"])
[['상품은 구매 날짜부터 12개월 동안 보증됩니다.']]

PersistentClient는 데이터를 메모리가 아니라 디스크에 저장하므로, 재시작할 때도 데이터가 유지됩니다. collection.add는 문서에 대한 임베딩을 자동으로 계산합니다(Chroma에는 기본 임베딩 함수가 있습니다). collection.query는 질문과 벡터적으로 가장 유사한 문서를 찾습니다. 더 큰 데이터셋의 경우 Docker에서 로컬로 실행되는 Qdrant로 전환할 수 있습니다. API는 유사하지만 중간 규모 확장에 더 최적화되어 있습니다.

6. LLM 계층: Ollama에서 로컬 모델 실행

이것이 API 비용에서 벗어나는 핵심 계층입니다. Ollama를 사용하면 Llama, Mistral, Gemma를 컴퓨터에서 직접 실행할 수 있으며, 요청 수 제한이 없습니다.

ollama pull llama3.1
ollama run llama3.1 "보증 정책을 한 문장으로 요약하세요."
상품은 구매 날짜부터 12개월 동안 보증됩니다.

ollama pull 명령어는 모델을 컴퓨터에 다운로드합니다(모델은 일반적인 CPU/GPU에서 실행하도록 양자화되어 있습니다). ollama run 명령어는 모델을 시작하고 프롬프트를 보내며, 결과를 터미널에서 직접 반환합니다. Ollama는 또한 localhost:11434에서 REST API를 열므로 Python이나 Java 코드에 통합할 수 있습니다.

백엔드가 Spring Boot인 경우 RestClient를 통해 Ollama를 호출하는 것이 매우 간단합니다:

import org.springframework.web.client.RestClient;

public class OllamaClient {

    private final RestClient restClient = RestClient.create("http://localhost:11434");

    public String generate(String model, String prompt) {
        var response = restClient.post()
                .uri("/api/generate")
                .body(new OllamaRequest(model, prompt, false))
                .retrieve()
                .body(OllamaResponse.class);
        return response.response();
    }

    record OllamaRequest(String model, String prompt, boolean stream) {}
    record OllamaResponse(String response) {}
}

OllamaClient는 Ollama에 대한 HTTP 호출을 캡슐화합니다. Spring 6의 RestClient는 이전 RestTemplate을 대체하며 더 간결합니다. OllamaRequest는 stream = false로 설정하여 각 토큰을 스트리밍하는 대신 한 번에 전체 응답을 받습니다. 데모 예제에서 처리하기 더 쉽습니다.

7. Tool 사용 via MCP (Model Context Protocol)

에이전트가 LLM과만 대화하면 실제로 할 수 있는 일이 없습니다. MCP는 에이전트가 파일 읽기, 데이터베이스 쿼리, GitHub API 호출 등 외부 도구를 호출할 수 있게 해주는 통신 표준입니다. 통일된 인터페이스로 각 도구마다 별도의 통합을 작성할 필요가 없습니다.

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data/docs"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"]
    }
  }
}

이 설정 파일은 두 개의 MCP 서버를 선언합니다. 하나는 파일 시스템(/data/docs에서 파일 읽기/쓰기), 하나는 GitHub(이슈 생성, PR 읽기)입니다. 당신의 에이전트(LangGraph, Claude Code 또는 MCP를 지원하는 모든 클라이언트)는 단순히 이 서버들에 연결하고 표준 스키마에 따라 도구를 호출하면 됩니다. 각 서비스마다 별도의 API 래퍼를 작성할 필요가 없습니다.

참고: npx를 통해 로컬에서 실행되는 MCP 서버는 라이선스 비용이 들지 않지만, 일부 서버는 액세스 토큰(예: GitHub 토큰)이 필요합니다. 해당 토큰은 여전히 원본 서비스에서 얻어야 합니다.

8. 코드 에이전트: Aider

에이전트가 저장소의 코드를 직접 작성하고 수정하길 원할 때 Aider는 오픈 소스 CLI 도구로 Ollama를 통한 로컬 모델과 잘 작동합니다.

pip install aider-chat
aider --model ollama/llama3.1 --file src/main/java/OrderService.java

위 명령은 Aider를 열고 Ollama를 통해 llama3.1 모델을 사용하도록 지정하며(비용이 드는 API를 통하지 않고), OrderService.java 파일을 미리 컨텍스트에 로드합니다. 여기서부터 요청을 한국어 또는 영어로 입력하면(‘음수 수량 유효성 검사 추가’), Aider가 diff를 생성하고 당신이 동의하면 자동으로 커밋합니다.

9. 데이터 계층: SQLite, DuckDB, Supabase

모든 AI 시스템이 Postgres 클러스터가 필요한 것은 아닙니다. SQLite는 작은 구조화된 스토리지에 충분하고, DuckDB는 서버 없이 파일 형식 데이터(CSV, Parquet) 분석에 강력합니다.

import duckdb

con = duckdb.connect()
result = con.execute("""
    SELECT category, COUNT(*) AS total
    FROM read_csv_auto('orders.csv')
    GROUP BY category
    ORDER BY total DESC
""").fetchall()
print(result)
[('electronics', 120), ('books', 45), ('clothing', 30)]

DuckDB는 read_csv_auto를 사용하여 CSV 파일을 직접 읽으므로 ETL이나 별도 데이터베이스로의 가져오기가 필요 없습니다. SQL 쿼리는 수십만 행의 데이터에 대해 거의 즉시 실행됩니다. 대부분의 내부 분석 사용 사례에 충분합니다. REST API와 인증이 필요한 백엔드가 필요하면 Supabase 무료 계층은 실시간 및 스토리지를 포함한 관리형 Postgres를 제공합니다.

10. 배포: Docker, Cloudflare Workers, Hugging Face

Docker를 사용한 컨테이너화는 개발 머신과 무료 호스트 머신 간에 일관된 배포를 보장합니다.

# docker-compose.yml
services:
  ollama:
    image: ollama/ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama

  app:
    build: .
    ports:
      - "8000:8000"
    depends_on:
      - ollama

volumes:
  ollama_data:

이 파일은 두 개의 서비스를 정의합니다. ollama는 LLM 모델을 실행하고, app은 당신의 백엔드이며 추론을 호출하기 위해 ollama에 의존합니다. docker compose up은 두 서비스를 동시에 시작합니다. 볼륨 ollama_data는 컨테이너 재시작을 통해 풀받은 모델을 유지합니다. 공개 데모가 필요하면 프론트엔드를 Cloudflare Workers(엣지, 넉넉한 무료 계층)로 푸시하고 작은 모델을 Hugging Face Spaces로 푸시하면 다른 사람들이 자신의 머신을 구성할 필요 없이 테스트할 수 있습니다.

11. 전체 스택 연결하기

실제 실행 흐름: 사용자가 Streamlit에 질문을 입력합니다 → LangGraph가 요청을 받고 Chroma를 호출하여 관련 컨텍스트를 검색합니다 → 컨텍스트를 프롬프트에 삽입하고 Ollama에 전송합니다 → 도구가 필요한 경우(파일 읽기, DB 쿼리) 에이전트가 MCP를 통해 호출합니다 → 결과가 UI로 반환됩니다. 모든 것이 한 머신 또는 컨테이너에서 실행되며, 비용이 드는 API로 나가는 요청이 없습니다.

def answer_question(question: str) -> str:
    docs = collection.query(query_texts=[question], n_results=2)
    context = "\n".join(docs["documents"][0])
    prompt = f"다음 정보를 기반으로:\n{context}\n\n답변: {question}"
    import ollama
    response = ollama.generate(model="llama3.1", prompt=prompt)
    return response["response"]

이 함수는 RAG의 “핵심”입니다. Chroma에서 컨텍스트를 가져와서 프롬프트에 삽입하고 Ollama를 호출하여 최종 답변을 생성합니다. 이것이 가장 기본적인 검색-후-생성 패턴이며, 사용 사례가 간단하면 복잡한 프레임워크가 필요 없습니다.

12. 데이터 처리 부분에 대한 테스트 작성

RAG 파이프라인에서 테스트하기 가장 쉬운 부분은 일반적으로 벡터 임베딩 전 텍스트 분할(청킹) 단계입니다. Java 백엔드가 이 단계를 처리하는 경우, 간단한 단위 테스트는 분할 시 off-by-one 오류를 방지합니다.

import java.util.List;
import java.util.ArrayList;

public class DocumentChunker {

    public List<String> chunk(String text, int maxLength) {
        List<String> chunks = new ArrayList<>();
        for (int i = 0; i < text.length(); i += maxLength) {
            chunks.add(text.substring(i, Math.min(i + maxLength, text.length())));
        }
        return chunks;
    }
}
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

class DocumentChunkerTest {

    @Test
    void shouldSplitTextIntoFixedSizeChunks() {
        DocumentChunker chunker = new DocumentChunker();

        List<String> chunks = chunker.chunk("상품은 전체 상품에 대해 12개월 동안 보증됩니다", 10);

        assertThat(chunks).hasSizeGreaterThan(1);
        assertThat(chunks.get(0)).hasSize(10);
        assertThat(String.join("", chunks)).isEqualTo("상품은 전체 상품에 대해 12개월 동안 보증됩니다");
    }
}

이 테스트는 세 가지를 확인합니다. 긴 텍스트가 여러 청크로 분할되고, 첫 번째 청크가 요청된 길이가 정확하며, 모든 청크를 다시 합치면 원본 텍스트와 정확히 일치해야 합니다. 문자가 손실되지 않아야 합니다. 이것은 청킹 로직을 직접 작성할 때 충분히 테스트하지 않으면 쉽게 발생하는 무음의 버그입니다.

13. 일반적인 오류와 이 스택을 언제 사용하면 안 되는지

  • 머신에 GPU가 없으면 로컬 모델은 클라우드 API보다 훨씬 느립니다. 일반적인 CPU에서 llama3.1 8B 요청은 5-15초가 걸릴 수 있습니다. 데모에는 허용되지만 즉시 응답이 필요한 제품에는 허용되지 않습니다.
  • 무료 계층에는 숨겨진 속도 제한이 있습니다. Vercel, Supabase, Cloudflare은 모두 월별 요청을 제한합니다. 임계값을 초과하면 대시보드를 모니터링하지 않으면 사전에 알지 못한 채 앱이 차단될 수 있습니다.
  • Chroma 임베디드는 다중 인스턴스에 적합하지 않습니다. 백엔드를 여러 포드로 확장하면 각 포드마다 자체 Chroma 파일이 있어서 불일치가 발생합니다. 이 경우 Qdrant 서버 또는 관리형 벡터 스토어로 전환해야 합니다.
  • 로컬 MCP 서버는 공격 표면을 증가시킵니다. 파일 시스템 MCP 서버는 에이전트가 파일을 읽고/쓸 수 있게 합니다. 인터넷에 잘못 노출되면 심각한 보안 취약점입니다.

참고: SLA가 있는 제품을 구축하거나 안정적으로 낮은 레이턴시가 필요하거나 처음부터 큰 트래픽을 예상한다면 이 0원 스택은 프로토타입 단계에만 사용해야 합니다. 실제로 운영할 때는 서비스 품질을 보장하기 위해 관리형 LLM API 또는 전용 GPU 인스턴스에 돈을 쓰는 것을 고려하세요.

14. 결론

0원 스택은 마케팅 기법이 아닙니다. 신용카드 없이 RAG, 에이전트, 도구 사용 및 배포가 완비된 완전한 AI 시스템을 구축할 수 있는 실제 오픈 소스 도구와 무료 계층의 조합입니다. 실제 장벽은 돈에 있지 않습니다. 각 계층을 올바르게 조합하는 방법을 이해하고, 무료 계층의 한계를 알고, 품질을 보장하기 위해 언제 유료 인프라로 전환해야 하는지 아는 것입니다. 이 스택으로 배우고 아이디어를 검증한 다음 제품이 실제로 확장해야 할 때 각 계층을 업그레이드하세요.

llmragollamalanggraphmcp