AI/ML

RTX 3090으로 Claude 대체하기 — Ollama + Caddy 인증 구축기

Claude API 비용이 부담되어 RTX 3090에 Ollama를 올리고 Caddy reverse proxy로 API 인증까지 구축한 과정. qwen3:30b의 환각 문제와 대응 전략까지 솔직하게 공유한다.

·8 min read
#Ollama#RTX 3090#Caddy#LLM#qwen3#self-hosted#Claude

A powerful GPU graphics card with RGB lighting

Claude 비용이 아프기 시작했다

BioAI Market에서 AI 기능은 핵심이다. 분석 결과 해석, 바이오마커 검색, 리포트 생성 등 여러 곳에서 LLM을 호출한다. 처음에는 Claude API를 썼는데, 개발 단계에서만 월 $30~50이 나갔다. 사용자가 늘면 감당이 안 될 게 뻔했다.

마침 집에 RTX 3090 24GB가 있었다. "이걸로 로컬 LLM을 돌리면 되지 않나?" — 그렇게 Ollama 전환 프로젝트가 시작됐다.

모델 선택 — qwen3:30b를 고른 이유

24GB VRAM에서 돌릴 수 있는 모델을 검토했다:

모델크기VRAM특징
llama3.1:8b4.7GB~6GB빠르지만 한국어 약함
qwen3:30b~18GB~20GBMoE, 실질 활성 ~8B, 한국어 OK
qwen3:32b~20GB~22GBDense, 느리지만 정확
deepseek-r1:32b~20GB~22GB추론 강하지만 느림

qwen3:30b를 선택한 결정적 이유는 MoE(Mixture of Experts) 아키텍처 때문이었다. 총 파라미터는 30B이지만 실제 활성 파라미터는 ~8B 수준이라, 30B급 지식을 가지면서도 8B급 속도로 추론이 가능했다. RTX 3090에서 체감 속도가 꽤 괜찮았다.

네트워크 구조 — 포트포워딩의 지옥

문제는 Ollama가 집 Windows 데스크톱에서 돌아가고, 서비스는 Vercel에 있다는 것이었다. Vercel에서 집 PC의 Ollama로 접근해야 한다.

최종 네트워크 경로:

Vercel (클라우드)
  ↓ HTTPS
공유기 (11434 포트포워딩)
  ↓
Windows Caddy (:11435)  ← API Key 검증
  ↓
Ollama (:11434)

공유기에서 외부 11434 포트를 Windows PC의 11435(Caddy)로 포워딩하고, Caddy가 인증 후 로컬 Ollama 11434로 프록시하는 구조다.

Caddy로 API 인증 추가 — Ollama의 치명적 약점

Ollama는 기본적으로 인증이 없다. http://localhost:11434에 아무나 요청을 보내면 응답한다. 이걸 인터넷에 노출하면? 누구든 내 GPU를 무료로 갖다 쓸 수 있다. 크립토 마이닝이라도 시키면 어쩌나.

Caddy를 reverse proxy로 넣어서 X-API-Key 헤더를 검증하게 만들었다.

Caddyfile:

:11435 {
    @valid_key header X-API-Key {env.OLLAMA_API_KEY}

    handle @valid_key {
        reverse_proxy localhost:11434
    }

    handle {
        respond "Unauthorized" 401
    }

    log {
        output file /var/log/caddy/ollama-proxy.log
        format json
    }
}

Windows에서 환경변수로 API 키를 설정:

$env:OLLAMA_API_KEY = "your-secret-key-here"
caddy run --config Caddyfile

이제 X-API-Key 헤더 없이 요청하면 401이 돌아온다:

# 인증 없이 → 401
curl http://my-ip:11434/api/generate
# Unauthorized

# 인증 있으면 → 200
curl -H "X-API-Key: your-secret-key-here" http://my-ip:11434/api/generate \
  -d '{"model": "qwen3:30b", "prompt": "hello"}'
# {"response": "Hello! How can I help you today?", ...}

Vercel 소스 14개 파일 수정 — 노가다의 현장

Vercel의 Next.js 코드에서 Ollama를 호출하는 모든 곳에 API 키 헤더를 추가해야 했다. grep -r "ollama" 해보니 14개 파일이 나왔다.

$ grep -rn "OLLAMA" --include="*.ts" --include="*.tsx" src/
src/lib/ollama.ts:5
src/app/api/chat/route.ts:12
src/app/api/analysis/interpret/route.ts:8
src/app/api/biomarker/search/route.ts:15
... (14개)

일괄 수정이 필요했다. 공통 Ollama 클라이언트를 만들었다:

// src/lib/ollama.ts
const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || 'http://localhost:11434'
const OLLAMA_API_KEY = process.env.OLLAMA_API_KEY || ''

export async function ollamaGenerate(params: {
  model: string
  prompt: string
  system?: string
  stream?: boolean
}) {
  const response = await fetch(`${OLLAMA_BASE_URL}/api/generate`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': OLLAMA_API_KEY,  // Caddy 인증용
    },
    body: JSON.stringify(params),
  })

  if (!response.ok) {
    throw new Error(`Ollama error: ${response.status} ${response.statusText}`)
  }

  return response.json()
}

그리고 14개 파일의 직접 fetch 호출을 전부 이 함수로 교체했다. 처음부터 이렇게 했으면 좋았을 텐데.

환각과의 전쟁 — qwen3:30b의 그림자

Ollama가 돌아가기 시작하고 나서 진짜 문제가 드러났다. 환각(hallucination)이 심했다.

프로테오믹스 분석 결과를 해석해달라고 하면 이런 답이 돌아왔다:

"BRCA1 단백질의 log2FC가 2.3으로 유의미하게 상향 조절되었으며, 
이는 Smith et al. (2023, Nature Proteomics)의 연구와 일치합니다..."

문제: 데이터에 BRCA1이 없었다. log2FC 2.3도 날조였다. "Smith et al." 논문도 존재하지 않았다.

환각 유형을 분류해보니:

  1. 없는 단백질 생성 — 데이터에 없는 단백질 이름을 만들어냄
  2. 수치 변조 — log2FC, p-value 등을 그럴듯하게 바꿈
  3. 가짜 논문 인용 — 존재하지 않는 저자, 저널, 연도 조합
  4. 근거 없는 연관성 — DB에 없는 바이오마커-질병 연관성 주장

Anti-Hallucination 프롬프트 6가지 규칙

프롬프트 엔지니어링으로 환각을 줄이려고 시도했다:

const ANTI_HALLUCINATION_SYSTEM = `You are a proteomics data analyst. Follow these 6 absolute rules:

1. ONLY mention proteins that appear in the provided data. Never invent protein names.
2. ONLY quote numerical values (log2FC, p-value, etc.) exactly as provided. Never round, estimate, or fabricate.
3. NEVER cite specific papers, authors, or journals. Say "literature suggests" if needed.
4. NEVER claim biomarker-disease associations unless explicitly provided in the input data.
5. If you are unsure about something, say "insufficient data to conclude" instead of guessing.
6. Every claim must be directly traceable to the input data. If it's not in the data, don't say it.`

결과: 약 70% 개선됐다. 대부분의 경우 데이터에 있는 단백질만 언급했고, 가짜 논문 인용은 거의 사라졌다. 하지만 가끔 여전히 수치를 미세하게 바꾸거나, 없는 연관성을 주장하는 경우가 있었다.

결론 — 채팅은 OK, 분석 리포트는 템플릿이 답

6개월간 운영하며 내린 결론:

  • 일반 채팅/Q&A: Ollama(qwen3:30b)로 충분. 비용 절감 효과 큼.
  • 분석 결과 해석/리포트: LLM에게 직접 쓰게 하면 안 된다. 템플릿 기반 렌더링이 안전하다.

Claude → Ollama 전환으로 API 비용은 $0이 됐다. 전기세를 제외하면 진정한 무료다. 하지만 "싼 게 비지떡"이라고, 품질과 안정성에서 트레이드오프가 있다는 걸 잊으면 안 된다.

환각 문제의 최종 해결책은 다른 글에서 더 자세히 다룬다.


참고 링크:

관련 글