← 홈
Research

노트북에 Local LLM 구축하기 (Ollama + Open WebUI)

#Research

안녕하세요 novice-22입니다.
이번 글은 노트북에 Local LLM을 직접 구축한 과정을 정리한 글입니다. Local LLM이 무엇인지부터 시작해서 Ollama로 모델을 실행하는 방법, 모델을 고른 기준, 설치 방법, 실제 테스트 결과, 그리고 돌려보면서 겪은 문제까지 정리했습니다.

1. Local LLM

1-1 Local LLM이란?

ChatGPT나 Claude 같은 상용 AI는 질문과 답변이 모두 AI 회사의 서버로 전송되어 처리된다. 이와 반대로 Local LLM은 공개된 모델을 내 PC에 내려받아 내 컴퓨터에서 직접 AI를 돌리는 방식이다.

구분클라우드 LLMLocal LLM
실행 위치AI 회사의 서버내 PC
입력한 데이터서버로 전송된다내 PC에만 남는다
비용구독료, API 사용료추가 비용 없음
인터넷필요모델을 받은 뒤에는 불필요
성능최상위 모델 사용 가능PC 사양에 맞는 모델만 가능

Local LLM을 사용하는 가장 큰 이유는 데이터가 내 PC에만 남는다는 점이다. 민감한 정보나 개인적인 내용이 외부로 나가지 않기 때문에 기밀 유지와 사생활 보호에 유리하다. 반면 내 PC에서 AI를 돌리는 것이기 때문에 PC의 사양에 따라 돌릴 수 있는 모델과 속도가 달라진다. 더 큰 모델을 돌릴수록 그래픽카드의 VRAM(그래픽카드가 없다면 메모리)이 많이 필요하기 때문에 가성비는 좋은 편은 아닌 것 같다.

1-2 Ollama란?

Ollama는 내 PC에서 LLM을 다운로드하고 실행할 수 있게 만든 오픈소스 플랫폼이다.
스마트폰에 비유하면
앱스토어와 앱 실행을 한 번에 해주는 것과 비슷하다. 모델을 고르고 받는 일과 실행하는 일을 Ollama가 대신 처리한다.
참고로 Ollama 안에서 모델을 실제로 돌리는 엔진은 llama.cpp이고, 모델 파일은 GGUF라는 형식을 쓴다. 이 부분은 몰라도 Ollama를 쓰는 데 문제없다.
설치하면 백그라운드에서 서버가 하나 뜬다. 기본 주소는
127.0.0.1:11434 이고, 터미널 명령도, 뒤에서 설치할 Open WebUI도 전부 이 서버에 요청을 보낸다.

자주 쓰는 명령은 다음과 같다.

명령하는 일
ollama pull <모델>모델 내려받기
ollama run <모델>모델 실행, 대화
ollama list받은 모델 목록
ollama ps지금 메모리에 올라가 있는 모델
ollama stop <모델>메모리에서 내리기
ollama create <이름> -f <Modelfile>설정을 바꾼 모델 만들기
ollama rm <모델>모델 삭제

모델은 메모리에 항상 올라가 있는 것이 아니다. 첫 요청이 들어올 때 올라가고, 기본값으로 5분 동안 요청이 없으면 내려간다. 그래서 Open WebUI 화면을 켜두는 것만으로는 모델이 메모리를 차지하지 않는다.

출처

1-3 기본 용어

모델 이야기로 넘어가기 전에 자주 나오는 용어부터 정리한다. 2장부터 나오는 모델 이름과 숫자는 전부 이 용어들로 읽을 수 있다.

파라미터와 B

LLM은 결국 엄청나게 많은 숫자의 묶음이다. 학습하는 동안 이 숫자들이 조금씩 조정되면서 언어와 지식을 익히는데, 이 숫자 하나하나를 파라미터(또는 가중치)라고 부른다. 모델이 아는 것은 전부 이 숫자들 안에 들어 있다.

B는 Billion, 즉 10억이다. 모델 이름에 붙은 숫자는 파라미터가 몇 개인지를 뜻한다.

표기파라미터 수예시
9B90억 개Qwen3.5 9B (이 글에서 최종 선택)
30B300억 개Qwen3-Coder 30B (처음 고른 모델)
671B6,710억 개DeepSeek V3 (서버급)

보통 B가 클수록 아는 것이 많고 답이 정확하지만, 그만큼 메모리를 많이 쓰고 느려진다. 노트북에서 모델을 고른다는 것은 결국 내 메모리에 들어가는 가장 큰 B를 찾는 일이다.

B와 GB의 관계

B는 파라미터 개수이고 GB는 파일 크기다. 둘은 파라미터 하나를 몇 비트로 저장하느냐에 따라 연결된다. 같은 9B 모델이라도 저장 방식에 따라 크기가 세 배 가까이 차이 난다.

저장 방식파라미터 하나9B 모델 크기
FP16 (원본)16비트19GB
Q8약 8비트11GB
Q4약 4비트6.6GB

Ollama에 올라온 huihui_ai/qwen3.5-abliterated:9b 의 실제 크기다. Q4 기준으로는 대략 B 숫자 × 0.6~0.7 = GB로 어림할 수 있다. 9B는 약 6GB, 30B는 약 19GB다.

양자화

위 표처럼 파라미터를 더 적은 비트로 줄여서 저장하는 것을 양자화라고 한다. 사진을 JPEG로 압축하는 것과 비슷하다. 많이 줄일수록 파일은 작아지지만 화질, 즉 답변 품질이 조금씩 떨어진다.

모델 이름 뒤에 붙는 q4_K_M 같은 표기가 양자화 방식이고, 앞의 숫자가 비트 수다. 뒤의 K_M 은 압축 방식의 세부 종류라 몰라도 된다.

토큰

LLM은 글을 글자나 단어 단위가 아니라 토큰 단위로 읽고 쓴다. 토큰은 단어를 잘게 쪼갠 조각이다. 영어는 단어 하나가 대략 토큰 하나이고, 한글은 영어보다 토큰을 더 많이 쓰는 편이다.

속도도 토큰으로 잰다. 초당 토큰(tok/s)은 1초에 토큰을 몇 개 만들어내는지를 뜻하고, 이 글에서 말하는 속도는 전부 이 단위다.

컨텍스트와 KV 캐시

컨텍스트는 모델이 한 번에 기억할 수 있는 대화 길이다. 토큰 단위로 세며 16K면 약 16,000토큰이다. 대화가 이보다 길어지면 앞부분부터 잊는다.

모델은 대화 내용을 따로 메모리에 담아두고 다음 토큰을 만들 때 참고하는데, 이 공간을 KV 캐시라고 한다. 컨텍스트를 길게 잡을수록 KV 캐시가 커져서 메모리를 더 쓴다.

RAM과 VRAM

모델은 실행하는 동안 통째로 메모리에 올라간다. 그래픽카드가 있으면 그래픽카드 전용 메모리인 VRAM에 올리고, 그래픽카드가 없거나 내장 GPU라면 일반 메모리인 RAM을 쓴다. VRAM이 모자라면 일부를 RAM에 나눠 올리는데, 이때는 많이 느려진다. 어느 쪽이든 모델이 들어갈 만큼 메모리가 있어야 실행할 수 있다.

모델 이름 읽는 법

이 용어들을 알면 모델 이름을 읽을 수 있다. 이 글에서 사용한 두 모델을 예로 들면 다음과 같다.

Qwen3-Coder-30B-A3B-Instruct

부분뜻
Qwen3알리바바가 만든 Qwen 모델의 3세대
Coder코드 작성에 특화해서 학습한 버전
30B전체 파라미터 300억 개
A3B한 번에 쓰는 파라미터 30억 개 (2-2의 MoE에서 설명)
Instruct질문에 답하도록 추가로 학습한 대화용 버전

huihui_ai/qwen3.5-abliterated:9b

부분뜻
huihui_ai모델을 올린 사람(계정)
qwen3.5Qwen 3.5세대
abliterated거절 반응을 제거한 버전 (2-3에서 설명)
:9b태그. 파라미터 90억 개. 양자화를 따로 적지 않으면 Q4다

2. 모델 고르기

2-1 노트북 사양

항목사양
CPUIntel Core Ultra 7 258V (8코어, Lunar Lake)
GPUIntel Arc 140V 내장 GPU
RAM32GB LPDDR5X-8533 (대역폭 약 136GB/s)
OSWindows 11 Home

내 노트북에는 GPU 전용 메모리(VRAM)가 없다. 내장 GPU가 시스템 메모리를 같이 쓰는 구조라 모델도 결국 32GB RAM 안에 올라가야 한다. 윈도우와 기본 프로그램이 이미 8GB 정도를 쓰고 있어서 실제로 쓸 수 있는 메모리는 20GB 남짓이다.

2-2 모델을 고르기 전에 알아야 할 것

크기는 메모리가 정한다

모델은 실행하는 동안 통째로 메모리에 올라간다. 모델 파일 크기에 대화 내용을 담는 캐시(KV 캐시)가 더해진 만큼 메모리를 쓴다. 메모리가 모자라면 디스크를 메모리 대신 쓰게 되는데, 이때는 PC 전체가 멈춘 것처럼 느려진다.

속도는 메모리 대역폭이 정한다

모델은 토큰 하나를 만들 때마다 가중치를 메모리에서 읽어온다. 그래서 생성 속도의 상한은 대략 메모리 대역폭 ÷ 토큰 하나에 읽는 양 이다. 이 노트북의 대역폭은 약 136GB/s라서 6GB짜리 모델이면 이론상 초당 22토큰 정도가 한계다.

양자화 선택 (Q4, Q3)

1-3에서 설명한 양자화는 크기와 품질을 맞바꾸는 선택이다. Q4가 크기와 품질의 균형이 좋아서 보통 기본값으로 쓰고, Q3는 메모리가 부족할 때 쓴다. 코드는 한 글자만 틀려도 동작하지 않기 때문에 일반 대화보다 양자화 손실이 잘 드러난다.

일반 모델과 MoE

일반(dense) 모델은 토큰마다 모든 가중치를 계산한다. MoE(Mixture of Experts) 모델은 가중치를 여러 전문가로 나눠두고 토큰마다 일부만 골라서 계산한다. 예를 들어 Qwen3-Coder-30B-A3B는 전체 30B 중 토큰당 약 3B만 사용한다. 이름의 A3B가 활성 파라미터 3B라는 뜻이다.

그래서 MoE는 메모리는 전체 크기만큼 쓰지만 속도는 작은 모델처럼 빠르다. 이 특징이 뒤에서 모델을 바꿀 때 중요하게 작용한다.

2-3 후보 비교

보안 업무 보조용으로 쓸 계획이라 코드 능력과 거절 여부를 같이 봤다. 아래는 이 노트북 기준으로 정리한 후보다.

모델구조크기(Q4)
Qwen3-Coder 30B-A3BMoE약 19GB
Qwen3.5 9B일반약 6.6GB
Qwen2.5-Coder 7B일반약 5GB
gpt-oss 20BMoE약 13GB
Qwen3.6 35B-A3BMoE약 22~24GB
Qwen3.8 27B일반약 18GB
DeepSeek-R1 14B일반 (증류)약 9GB

이 중 메모리에 들어가면서 코드 능력이 가장 좋은 Qwen3-Coder 30B-A3B를 먼저 골랐다. Qwen3.5 9B는 메모리에 여유가 있는 대안이었고, 실제로 2-5에서 이 모델로 바꾸게 된다. 나머지는 다음 이유로 제외했다.

  • Qwen2.5-Coder 7B: 가볍지만 이전 세대라 코드 능력이 떨어진다
  • gpt-oss 20B: MoE라 빠르지만 안전 학습이 강해서 보안 관련 질문을 자주 거절한다
  • Qwen3.6 35B-A3B: Q4 기준 22GB가 넘어서 메모리에 들어가지 않는다
  • Qwen3.8 27B: 일반 구조라 토큰마다 27B를 전부 계산해서 너무 느리다
  • DeepSeek-R1 14B: 이름은 DeepSeek이지만 증류 모델이다. 증류는 큰 모델의 답변을 작은 모델이 따라 배우게 하는 학습 방식으로, 이 모델은 DeepSeek-R1(671B)의 답변으로 Qwen 14B를 학습시킨 것이다. 답하기 전에 추론을 길게 출력해서 체감 속도가 느리다

거절 제거(abliterated) 모델

보안 업무에서는 취약점의 원인이나 공격 기법을 묻는 일이 많은데, 일반 모델은 이런 질문을 거절하는 경우가 있다. abliterated는 모델 내부에서 거절 반응을 만드는 방향을 찾아서 제거한 커뮤니티 버전이다. 이번에는 huihui-ai가 공개한 abliterated 버전을 사용했다.

제작자가 안전장치가 없고 결과에 책임지지 않는다고 명시한 모델이다. 허가받은 업무 범위 안에서만 사용하고, 생성된 코드는 직접 검토한 뒤에 사용해야 한다.

2-4 처음 고른 모델: Qwen3-Coder 30B-A3B

코드 특화 모델이고 MoE라서 빠르다는 점 때문에 처음에는 이 모델을 골랐다. Q4는 19GB라 윈도우와 Open WebUI, 에디터를 같이 띄우면 32GB를 넘기기 때문에 Q3(14GB)로 받았다.

실측 결과는 좋았다.

항목결과
메모리 (ollama ps)16GB
생성 속도초당 25~29토큰
입력 처리 (1,569토큰 입력)초당 154토큰
처리 장치100% GPU

문제는 실제로 사용할 때였다. 모델을 올리면 남는 메모리가 2.6GB밖에 되지 않았고, 다른 프로그램 몇 개만 같이 켜도 PC가 멈췄다.

2-5 최종 선택: Qwen3.5 9B

그래서 파라미터 수가 3분의 1 수준인 Qwen3.5 9B로 바꿨다. Qwen3-Coder보다 나중에 나온 세대라 베이스가 더 최신이고, 이미지 인식(vision), 도구 호출(tools), 생각 모드(thinking)를 지원한다.

항목30B-A3B (Q3)9B (Q4)
메모리16GB약 6GB
모델을 올린 뒤 남는 메모리2.6GB약 10GB
생성 속도초당 25~29토큰초당 16~17토큰
처리 장치100% GPU100% GPU
토큰당 계산하는 크기약 3B9B 전체

여기서 볼 것은 더 작은 9B가 오히려 느리다는 점이다. 2-2에서 설명한 MoE 때문이다. 30B-A3B는 토큰마다 3B만 읽고 9B는 9B 전체를 읽는다. 크기를 줄여서 메모리 문제는 해결했지만 속도는 줄어들었다.

💡
메모리에 여유가 있는 PC라면 MoE 모델이 크기 대비 훨씬 빠르다. 메모리가 부족할 때만 작은 일반 모델로 변경하는 게 좋다.

출처


3. 설치

Windows 11, PowerShell 기준으로 진행한다.

3-1 Ollama 설치

winget install Ollama.Ollama
ollama --version

설치 후 처음 실행하면 계정을 만들라는 화면이 나온다. 계정은 클라우드 모델을 쓸 때 필요한 것이라 로컬에서만 사용한다면 아래의 No thanks, I'll use Ollama locally를 누르면 된다.

서버가 외부에 열려 있지 않은지도 확인한다.

netstat -ano | findstr 11434

127.0.0.1:11434 로 나오면 내 PC에서만 접속된다.

3-2 모델 받기

ollama pull huihui_ai/qwen3.5-abliterated:9b

컨텍스트 길이 지정

1-3에서 설명한 컨텍스트 길이를 정해준다. 이 모델은 256K까지 지원하지만 길게 잡을수록 KV 캐시가 메모리를 더 쓴다. 코드 몇 백 줄을 다루기에 충분한 16K로 지정했다.

설정은 Modelfile이라는 파일에 적는다. 이름은 자유롭게 정하면 되고, 여기서는 qwen35-9b-pt.Modelfile 로 만들었다.

FROM huihui_ai/qwen3.5-abliterated:9b
PARAMETER num_ctx 16384

FROM 은 바탕이 될 모델이고 PARAMETER num_ctx 가 컨텍스트 길이다. 이 파일로 새 모델을 만든다.

ollama create qwen35-9b-pt -f qwen35-9b-pt.Modelfile
ollama show qwen35-9b-pt

ollama show 의 Capabilities에서 이 모델이 지원하는 기능을 확인할 수 있다.

Capabilities
  completion
  vision
  tools
  thinking

이렇게 만들면 원본과 새로 만든 모델이 목록에 같이 보인다. 다만 같은 파일을 공유하기 때문에 디스크는 한 번만 차지한다.

3-3 내장 GPU 사용 설정

모델을 받은 뒤 실행하고 ollama ps 를 보면 PROCESSOR 칸에 100% CPU 로 나왔다. GPU를 전혀 쓰지 않은 것이다. 로그에 이유가 적혀 있다.

dropping integrated GPU; to enable, set OLLAMA_IGPU_ENABLE=1

Ollama는 Vulkan으로 Intel Arc 140V를 인식하지만 내장 GPU는 기본으로 제외한다. 로그는 %LOCALAPPDATA%\Ollama\server.log 에 있다.

환경 변수를 등록하고 Ollama를 다시 실행하면 된다.

[Environment]::SetEnvironmentVariable("OLLAMA_IGPU_ENABLE", "1", "User")

작업 표시줄 아이콘에서 Ollama를 완전히 종료한 뒤 다시 실행하고 확인한다.

ollama run qwen35-9b-pt "안녕" --think=false --verbose
ollama ps

--verbose 를 붙이면 답변 뒤에 생성 속도(eval rate)가 나오고, --think=false 는 5-4에서 설명할 thinking을 끄는 옵션이다. PROCESSOR가 100% GPU 로 바뀌면 된다.

2-4의 30B 모델로 생성 속도를 비교하면 CPU만 쓸 때 초당 22.4토큰, 내장 GPU를 쓸 때 초당 25~29토큰이었다. 생각보다 차이가 작은데, 내장 GPU도 CPU와 같은 메모리를 쓰기 때문이다. 2-2에서 설명한 것처럼 생성 속도는 메모리 대역폭이 정하므로 비슷하게 나온다.

GPU의 덕을 크게 보는 쪽은 입력 처리다. 입력 처리는 계산량이 많아서 GPU가 유리하고, 실제로 1,569토큰짜리 입력을 초당 154토큰으로 처리했다. 코드를 붙여넣고 질문하는 일이 많다면 이 차이가 체감된다.

3-4 Open WebUI 설치

Open WebUI는 ChatGPT와 비슷한 대화 화면을 제공하는 오픈소스 웹 앱이다. Ollama 서버에 붙어서 모델 선택, 대화 기록, 이미지 첨부 같은 기능을 쓸 수 있게 해준다.

윈도우가 아니라 WSL에 설치한 이유

처음에는 윈도우에 바로 설치하려 했지만 uv가 내려받은 Python이 실행되지 않았다.

error: Querying Python at `...\cpython-3.11.16-windows-x86_64-none\python.exe` failed with exit status exit code: 0xc0e90002

확인해보니 윈도우 11의 스마트 앱 컨트롤이 켜져 있었다. 스마트 앱 컨트롤은 신뢰가 확인되지 않은 실행 파일을 차단한다. 스마트 앱 컨트롤을 끄지 않고 해결하기 위해 WSL(Ubuntu) 안에 설치했다.

WSL 네트워크 설정

WSL 안의 Open WebUI가 윈도우의 Ollama(127.0.0.1:11434)에 접속하려면 WSL이 윈도우와 네트워크를 공유해야 한다. %USERPROFILE%\.wslconfig 파일에 다음 내용을 적는다.

[wsl2]
networkingMode=mirrored
memory=6GB

[experimental]
autoMemoryReclaim=dropCache
설정설명
networkingMode=mirroredWSL의 localhost 가 윈도우와 같아진다. Ollama를 외부에 열지 않고도 WSL에서 접속할 수 있다
memory=6GBWSL이 쓸 수 있는 메모리 상한
autoMemoryReclaim=dropCacheWSL이 다 쓴 캐시 메모리를 윈도우로 돌려준다

마지막 설정이 없으면 WSL이 한 번 쓴 메모리를 잘 돌려주지 않는다. 실제로 Open WebUI를 설치한 직후 WSL이 6.1GB를 잡고 있었다. 저장한 뒤 wsl --shutdown 으로 WSL을 재시작해야 반영된다.

설치

WSL(Ubuntu) 터미널에서 진행한다.

curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv --python 3.11 ~/open-webui/venv
uv pip install --python ~/open-webui/venv/bin/python open-webui

설치 용량이 약 7GB로 크기 때문에 시간이 꽤 걸린다.

실행

export OFFLINE_MODE=true
export DO_NOT_TRACK=true
export SCARF_NO_ANALYTICS=true
export ANONYMIZED_TELEMETRY=false
export OLLAMA_BASE_URL=http://127.0.0.1:11434
~/open-webui/venv/bin/open-webui serve --host 127.0.0.1 --port 8080
설정설명
OFFLINE_MODE인터넷에서 모델이나 파일을 내려받지 않는다
DO_NOT_TRACK외 2개사용 통계 전송을 끈다
OLLAMA_BASE_URL연결할 Ollama 서버 주소
--host 127.0.0.1내 PC에서만 접속되게 한다

--host 127.0.0.1 을 빼먹으면 안 된다. 기본값은 0.0.0.0 이라 같은 네트워크의 다른 기기에서도 접속된다.

브라우저에서 http://127.0.0.1:8080 에 접속하면 가입 화면이 나온다. 처음 가입한 계정이 관리자가 되고, 관리자가 만들어지면 신규 가입은 자동으로 꺼진다.

자동 작업 끄기

Open WebUI는 답변이 끝날 때마다 같은 모델로 채팅 제목, 태그, 후속 질문을 생성한다. 로컬 모델에서는 이 작업들이 다음 질문을 크게 지연시키기 때문에 꺼두는 것이 좋다. 이유는 5-3에서 설명한다.

관리자 패널 → 상단 설정 → 왼쪽 메뉴 관리자 › 시스템 › 인터페이스로 들어가서, 생성 구역의 스위치를 전부 끄고 저장한다.

  • 제목 생성
  • 후속 질문 생성
  • 태그 생성
  • 자동완성 생성
  • 검색 쿼리 생성
  • 웹 검색 쿼리 생성

왼쪽 메뉴 위쪽의 개인용 › 인터페이스에도 제목 자동 생성 같은 비슷한 항목이 있지만, 그건 내 계정에만 적용되는 설정이다. 서버 전체에 적용하려면 관리자 시스템 설정에서 끄는 것이 맞다.

출처


4. 실행 화면

4-1 대화 화면

브라우저에서 http://127.0.0.1:8080 에 접속한 첫 화면이다. 가운데에 3-2에서 만든 qwen35-9b-pt 가 선택되어 있고, 입력창 왼쪽의 + 로 파일이나 이미지를 첨부할 수 있다.

4-2 메모리 사용량

4-3의 코드 작성 질문에 답변을 생성하는 동안의 작업 관리자 화면이다.

GPU 사용률이 96%까지 올라가 있어서 답변 생성을 내장 GPU가 처리하고 있다는 것을 확인할 수 있다. 내장 GPU는 전용 메모리가 없어서 시스템 메모리를 빌려 쓰는데, 이것이 공유 GPU 메모리다. 이 노트북은 최대 18GB까지 빌려 쓸 수 있고, 지금 쓰고 있는 9.4GB는 모델(5.9GB)에 브라우저 등 다른 프로그램이 쓰는 GPU 메모리까지 합친 값이다.

전체 메모리는 28.8/31.5GB(91%)까지 찼다. 브라우저와 에디터 등 다른 프로그램을 같이 켜둔 상태라 모델만 쓴 양은 아니지만, 9B 모델로도 여유가 많지 않다는 것을 보여준다. 켜둔 프로그램에 따라 남는 메모리가 크게 달라지므로 모델을 올리기 전에 안 쓰는 프로그램은 닫는 것이 좋다.

4-3 테스트

몇 가지 질문으로 모델을 테스트했다. 답변 아래 ⓘ 아이콘에 마우스를 올리면 생성 속도를 볼 수 있다.

코드 작성

파이썬으로 특정 폴더의 파일 목록을 크기순으로 정렬해서 출력하는 스크립트 작성해줘

동작하는 스크립트를 만들었지만 작은 버그가 하나 있었다. 주석에는 "빈 입력 시 현재 디렉토리로 간주"라고 적어 놓고, 실제 코드는 if folder and folder: 라서 엔터만 치면 아무것도 실행되지 않는다. list_files_by_size(folder or ".") 로 고쳐야 주석대로 동작한다. 답변 끝의 "입력 없이도 현재 디렉토리 사용 가능"이라는 설명도 그래서 사실과 다르다. 그럴듯해 보이는 코드라도 직접 읽고 실행해서 확인해야 하는 이유다.

코드 리뷰

취약점 두 개를 일부러 넣은 코드를 주고 찾게 했다.

아래 코드의 보안 취약점을 찾고, 위험도와 수정 방법을 알려줘
import sqlite3, os
from flask import Flask, request
app = Flask(__name__)

@app.route("/user")
def user():
    uid = request.args.get("id")
    conn = sqlite3.connect("app.db")
    return str(conn.execute(f"SELECT * FROM users WHERE id = {uid}").fetchall())

@app.route("/ping")
def ping():
    host = request.args.get("host")
    return os.popen("ping -c 1 " + host).read()

정답은 /user 의 SQL 인젝션과 /ping 의 명령어 인젝션이다.

모델은 바로 답하지 않고 분석 범위를 먼저 물어봤다(화면 상단의 ask_user). 여기서 "종합 분석"을 골랐다.

결과는 다음과 같다.

항목결과
취약점 찾기두 개 모두 찾음
위험도명령어 인젝션은 상, SQL 인젝션은 중으로 평가. SQL 인젝션도 DB 전체가 노출되므로 상이 맞다
/user수정파라미터 바인딩으로 고친 방향은 맞다. 하지만 id 가 없으면 users 테이블 전체를 반환하고, sqlite3.Row 에 없는 to_json() 을 호출해서 실행하면 에러가 난다
/ping수정허용 목록을 만들었지만, 목록에 없는 값도 따옴표로 감싸서 그대로 실행한다
설명"/tmp에 파일이 쌓이면 메모리 제한 공격" 같은 근거 없는 내용이 섞여 있다

가장 큰 문제는 /ping 수정 코드다. 큰따옴표로 감싸도 셸은 그 안의 $(명령) 과 백틱을 실행하기 때문에 host=$(id) 를 넣으면 그대로 명령이 실행된다. 고친 것처럼 보이지만 취약점은 그대로 남아 있다. 허용 목록에 없는 값은 거절해야 하고, 실행할 때도 subprocess.run(["ping", "-c", "1", host]) 처럼 셸을 거치지 않게 하는 것이 안전하다.

답변 뒤쪽에는 두 수정을 합친 전체 코드와 테스트 코드도 있었다. 그런데 테스트용으로 만든 /test 경로에 os.popen(f"echo 'hello {request.args.get('cmd', 'world')}'") 가 들어 있었다. 사용자 입력을 그대로 셸에 넘기는 코드라 새로운 명령어 인젝션을 만든 셈이다.

정리하면 9B 모델은 눈에 띄는 취약점은 잘 찾지만, 직접 짠 수정 코드에 취약점을 다시 넣는다. 취약점 찾기는 참고할 만하지만, 모델이 짠 수정 코드는 반드시 직접 검토한 뒤에 써야 한다.

이미지 인식

입력창의 + 로 3-3의 ollama ps 스크린샷을 첨부하고 화면을 설명하게 했다.

표의 7개 항목(이름, ID, 크기, 처리 장치, 컨텍스트, 실행 엔진, UNTIL)은 전부 정확하게 읽었다. 화면 위쪽 출력에서 입력 14토큰, 출력 29토큰이라는 숫자까지 찾아냈다. 다만 같은 화면에 있는 생성 속도(16.21 tokens/s)는 언급하지 않았다.

문제는 읽은 값을 설명하는 부분이다.

모델의 설명실제
UNTIL은 "다음 실행 예정 시간"4분 뒤 모델이 메모리에서 내려간다는 뜻 (1-2 참고)
100% GPU는 "GPU 메모리를 가득 채운 상태"모델 전체를 GPU에서 처리한다는 뜻
pt는 "Fine-tune 버전 (Plain/Pro)"3-2에서 직접 붙인 이름이라 모델이 알 수 없는데도 지어냈다
qwen35는 "Qwen-35"Qwen3.5
활용 팁의ollama ps --model qwen35존재하지 않는 옵션

화면의 글자와 숫자는 정확하게 읽지만, 그 의미를 설명할 때는 모르는 것도 그럴듯하게 지어낸다. 코드 리뷰에서 본 것과 같은 패턴이다.


5. 겪은 문제와 해결

5-1 GPU를 쓰지 않는다

ollama ps 에 100% CPU 로 나오면 내장 GPU가 제외된 것이다. 3-3의 OLLAMA_IGPU_ENABLE=1 로 해결했다.

5-2 30B 모델을 올리면 PC가 멈춘다

모델이 16GB를 차지해서 남은 메모리가 2.6GB였다. 다른 프로그램을 같이 쓰면 메모리가 모자라 디스크를 쓰게 되고 PC가 멈췄다. 2-5에서 설명한 것처럼 9B로 바꿔서 해결했다.

5-3 답변 하나에 몇 분이 걸린다

화면에는 "9초 동안 생각함"이라고 표시됐는데 실제로 답이 오기까지 몇 분이 걸렸다. 원인은 두 가지가 겹친 것이었다.

첫째, Ollama는 기본 설정에서 요청을 한 번에 하나씩 처리한다. 로그에 찍힌 서버 설정에서 OLLAMA_NUM_PARALLEL:1 을 확인할 수 있다.

둘째, Open WebUI는 답변이 끝나면 같은 모델로 제목, 태그, 후속 질문을 만드는 요청을 추가로 보낸다. Open WebUI 로그에서 generate_follow_ups 작업이 확인된다. 질문 하나에 실제 요청은 여러 개가 나가고, 다음 질문은 이 요청들이 전부 끝날 때까지 대기열에서 기다린다.

3-4의 자동 작업 끄기로 해결했다. 이후로는 질문 하나에 요청 하나만 나간다. 같은 이유로 답변이 끝나기 전에 다음 질문을 보내거나 다른 프로그램에서 같은 모델을 호출해도 밀린다. 실제로 요청이 몰렸을 때는 290토큰짜리 답변에 3분이 넘게 걸렸다.

5-4 thinking 때문에 첫 글자가 늦게 나온다

Qwen3.5는 thinking이 기본으로 켜져 있다. 답하기 전에 추론을 먼저 생성하는데, 그동안 화면에는 아무것도 나오지 않는다. 파일 목록 스크립트 질문에서는 답변 전에 추론만 467자를 먼저 만들었다.

API로 호출할 때는 "think": false 를 넣으면 끌 수 있다.

{
  "model": "qwen35-9b-pt",
  "messages": [{"role": "user", "content": "안녕"}],
  "think": false
}

Modelfile로는 끌 수 없다. PARAMETER think false 를 넣으면 Error: unknown parameter 'think' 가 난다.

5-5 Flash Attention을 켰더니 멈췄다

메모리를 아끼려고 OLLAMA_FLASH_ATTENTION=1 과 OLLAMA_KV_CACHE_TYPE=q8_0 을 켰다. KV 캐시가 줄어서 모델 메모리가 6.0GB에서 5.8GB로 줄었고 짧은 질문도 정상이었다.

그런데 긴 답변을 생성하던 도중 모델이 ollama ps 에서 Stopping... 상태로 멈췄고, 10토큰짜리 짧은 요청에도 응답하지 않았다. 두 설정을 지우고 Ollama를 재시작하자 정상으로 돌아왔다. 아낄 수 있는 메모리도 0.2GB뿐이라 이 노트북(Vulkan, 내장 GPU)에서는 두 설정을 쓰지 않는다.


6. 마무리

32GB 노트북에서 내장 GPU로 9B 모델을 초당 16~17토큰으로 돌릴 수 있었다. 가장 큰 제약은 메모리였다. 모델 크기는 메모리가, 속도는 메모리 대역폭이 정한다는 것과 MoE 모델이 크기 대비 빠르다는 것을 직접 확인할 수 있었다.

답변 품질은 테스트에서 본 것처럼 일의 종류에 따라 차이가 컸다. 취약점 찾기나 화면 글자 읽기처럼 답이 눈앞에 있는 일은 잘했지만, 수정 코드나 설명처럼 직접 만들어내야 하는 부분은 그럴듯하게 틀리는 경우가 많았다. Local LLM의 결과는 참고용으로 쓰고, 반드시 직접 확인해야 한다.

노트북에서는 연습과 사용법 익히기까지가 현실적인 범위다. 다음에는 집에 서버를 구축해서 더 큰 모델을 돌려볼 예정이다.