11장. 폐쇄망 문서 파이프라인과 폐쇄형 RAG
실습 1 — 오프라인으로 돌아가는지 확인하기
이 장의 모든 실습을 바깥 연결을 막은 채 돌리기 위해, 막는 코드를 out_offline.py 파일로 만들고 확인합니다. 임베딩 모델이 캐시에서 불러와지는지, Chroma 가 통계를 보내지 않는지, 사내 주소인 Ollama 는 열려 있는지 봅니다. 7장의 임베딩 모델(intfloat/multilingual-e5-small)을 한 번은 내려받아 두었어야 하고, Ollama 가 켜져 있어야 합니다. 주피터에서 따라 할 때는 커널을 새로 시작한 뒤에 돌립니다.
1단계. 바깥 연결을 막는 파일 만들기
from pathlib import Path
# 모든 실습이 첫머리에서 불러 쓸 모듈을 파일로 만든다
Path("out_offline.py").write_text('''"""폐쇄망 흉내: localhost 밖으로 나가는 연결을 막고, 막은 주소를 적어 둔다."""
import os
import socket
BLOCKED = [] # 막은 주소를 차례로 적는다
LOCAL = {"localhost", "127.0.0.1", "::1", None}
_real = socket.getaddrinfo # 주소를 IP 로 바꾸는 원래 함수
def _only_local(host, *args, **kwargs):
if host not in LOCAL:
BLOCKED.append(host)
raise OSError(f"폐쇄망: {host} 로 나가는 연결을 막았습니다")
return _real(host, *args, **kwargs)
def guard():
"""라이브러리의 오프라인 설정을 켜고, 바깥 연결을 막는다. 라이브러리를 import 하기 전에 부른다."""
os.environ["HF_HUB_OFFLINE"] = "1" # Hugging Face: 허브에 묻지 않고 캐시만 쓴다
os.environ["TRANSFORMERS_OFFLINE"] = "1"
os.environ["ANONYMIZED_TELEMETRY"] = "False" # Chroma: 사용 통계를 보내지 않는다
socket.getaddrinfo = _only_local
def release():
"""막기를 푼다."""
socket.getaddrinfo = _real
''', encoding="utf-8")
import sys
sys.path.insert(0, ".") # 작업 폴더(data)에 만든 out_offline.py 를 찾게 한다
import out_offline
out_offline.guard()
# 결과: 막기가 켜졌는지(바꿔 끼운 함수인지)
import socket
print("막기 켜짐:", socket.getaddrinfo is not out_offline._real)막기 켜짐: True2단계. 바깥은 막히고 안쪽은 열렸는지 보기
import urllib.request
# 바깥 주소: 연결하기 전에 막혀야 한다(실제로 인터넷에 나가지 않는다)
try:
urllib.request.urlopen("https://huggingface.co", timeout=5)
except Exception as e:
print("바깥:", type(e).__name__, e)
# 사내 주소(이 PC 의 Ollama): 열려 있어야 한다
print("안쪽:", urllib.request.urlopen("http://localhost:11434/api/version").read().decode())
print("막은 주소:", out_offline.BLOCKED)바깥: URLError <urlopen error 폐쇄망: huggingface.co 로 나가는 연결을 막았습니다>
안쪽: {"version":"0.40.1"}
막은 주소: ['huggingface.co']3단계. 모델과 벡터DB 를 오프라인으로 열기
from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.config import Settings
# 캐시에 있는 임베딩 모델을 부른다. 오프라인 설정 덕에 허브에 묻지 않는다
emb = SentenceTransformer("intfloat/multilingual-e5-small", device="cpu")
v = emb.encode("query: 반차는 언제까지 신청하나요?", normalize_embeddings=True)
print("임베딩:", v.shape)
# Chroma 를 통계 없이 연다. anonymized_telemetry=False 가 통계 보내기를 끈다
client = chromadb.EphemeralClient(settings=Settings(anonymized_telemetry=False))
print("Chroma 컬렉션 수:", len(client.list_collections()))
# 결과: 여기까지 새로 막힌 주소가 없으면, 모델과 DB 가 바깥을 찾지 않은 것이다
print("막은 주소:", out_offline.BLOCKED)임베딩: (384,)
Chroma 컬렉션 수: 0
막은 주소: ['huggingface.co']4단계. 들고 갈 모델 목록 만들기
import json
import pandas as pd
from huggingface_hub import scan_cache_dir
rows = []
# Hugging Face 캐시에 든 모델(이 과정에서 내려받은 것)과 크기
for repo in scan_cache_dir().repos:
rows.append({"종류": "HF", "이름": repo.repo_id, "크기MB": round(repo.size_on_disk / 1e6)})
# Ollama 에 든 모델과 크기(사내 주소이므로 열린다)
tags = json.loads(urllib.request.urlopen("http://localhost:11434/api/tags").read())
for m in tags["models"]:
rows.append({"종류": "Ollama", "이름": m["name"], "크기MB": round(m["size"] / 1e6)})
carry = pd.DataFrame(rows).sort_values(["종류", "크기MB"], ascending=[True, False])
carry.to_csv("out_carry_list.csv", index=False, encoding="utf-8-sig")
print(carry.to_string(index=False))
print("합계:", f"{carry['크기MB'].sum():,}MB") 종류 이름 크기MB
HF skt/kogpt2-base-v2 1029
HF Qwen/Qwen2.5-0.5B 1000
HF Qwen/Qwen2.5-0.5B-Instruct 1000
HF Helsinki-NLP/opus-mt-ko-en 628
HF intfloat/multilingual-e5-small 493
HF cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 493
Ollama qwen2.5:3b 1930
Ollama llama3.2:1b 1321
Ollama qwen2.5:1.5b 986
Ollama gemma3:1b 806
Ollama qwen2.5:0.5b 398
Ollama nomic-embed-text:latest 274
합계: 10,358MB이 실습이 뜻하는 것
이 실습은 보안 구역에 들어가기 전에 바깥 통신을 끊고, 들고 갈 물건을 점검하는 일과 같습니다. 출입구에 "바깥으로 거는 전화는 끊고 구내 전화만 잇는" 교환원을 세우는 규칙을 파일로 써 두고 켜 봅니다(1단계). 바깥 번호와 구내 번호로 각각 걸어 보고(2단계), 들고 들어간 사전(임베딩 모델)과 서류함(벡터DB)을 펼쳐도 바깥으로 전화를 걸지 않는지 보고(3단계), 마지막으로 들고 갈 물건 목록과 무게를 적습니다(4단계). 여기서 교환원이 guard() 가 바꿔 끼운 socket.getaddrinfo, 바깥 번호가 huggingface.co, 구내 번호가 localhost 의 Ollama, 물건 목록이 out_carry_list.csv 입니다. 벡터DB 의 DB 는 DataBase(데이터베이스)의 줄임입니다. 비유와 다른 점은, 진짜 폐쇄망은 네트워크 장비가 막지만 여기서는 파이썬 프로그램 하나 안에서만 막는 흉내라는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 막기 | 막기 켜짐: True | 주소를 찾는 함수가 바꿔 끼운 함수로 바뀌었습니다. 이제 localhost 밖의 주소는 연결 전에 막힙니다 |
| 2. 바깥 | URLError … huggingface.co 로 나가는 연결을 막았습니다 | 인터넷 주소로 가려 하자 실제로 나가기 전에 막혔습니다 |
| 2. 안쪽 | {"version":"0.40.1"} | 같은 PC 의 Ollama 는 판 번호로 답했습니다. 사내 주소는 열려 있다는 뜻입니다 |
| 2. 막은 주소 | ['huggingface.co'] | 지금까지 막은 주소는 방금 시험한 하나입니다 |
| 3. 모델과 DB | 임베딩: (384,), Chroma 컬렉션 수: 0 | 임베딩 모델이 캐시에서 불러와져 문장 하나를 숫자 384개로 바꿨고, Chroma 도 빈 상태로 열렸습니다 |
| 3. 막은 주소 | ['huggingface.co'] 그대로 | 모델과 DB 를 여는 동안 새로 막힌 주소가 없습니다. 둘 다 바깥을 찾지 않았습니다 |
| 4. 들고 갈 목록 | HF 모델 6개, Ollama 모델 6개, 합계: 10,358MB | HF 는 Hugging Face 입니다. 이 PC 에 든 모델을 모두 적으면 10GB 남짓입니다. 이 교안에서 쓰지 않는 모델도 섞여 있어 실제 반입 목록은 여기서 골라 만듭니다 |
한 줄로 줄이면, 바깥 연결을 막은 채로도 모델과 벡터DB 가 캐시와 사내 주소만으로 돌아간다는 것을 확인한 실습입니다. 여기서 만든 out_offline.py 를 실습 4, 6과 프로젝트 1이 첫머리에서 불러 씁니다. 다만 막은 것은 이 파이썬 프로그램 안의 주소 찾기뿐이라, PC 전체가 폐쇄망이 되었거나 다른 프로그램의 연결까지 막혔다는 뜻은 아닙니다.
코드 설명
socket.getaddrinfo는huggingface.co같은 이름을 IP 로 바꾸는 함수입니다. 바깥으로 나가는 연결은 대부분 이 함수를 먼저 거치므로, 바꿔 끼우면 연결하기 전에 막을 수 있습니다.- 2단계에서
huggingface.co로 가는 연결은 이름을 IP 로 바꾸는 단계에서 막혀URLError가 났고, 막은 주소에huggingface.co하나가 적혔습니다. 실제로 인터넷에 나가기 전에 막힌 것입니다. 같은 때localhost의 Ollama 는 판(0.40.1)을 돌려줬습니다. - 3단계에서 임베딩 모델을 부르고 Chroma 를 연 뒤에도 막은 주소는
huggingface.co하나 그대로입니다. 두 라이브러리가 바깥을 찾지 않았다는 뜻입니다. 실행하면 "Could not reach the Hub … offline mode is enabled" 라는 알림이 나오는데, 허브에 묻지 않고 캐시를 쓴다는 뜻이라 오류가 아닙니다. scan_cache_dir()는 Hugging Face 캐시에 든 모델과 크기를, Ollama 의/api/tags는 Ollama 모델과 크기를 돌려줍니다. 이 PC 에는 Hugging Face 모델 6개와 Ollama 모델 6개, 모두 10,358MB 가 있었습니다. 이 교안에서 쓰지 않는skt/kogpt2-base-v2와nomic-embed-text도 들어 있으니, 반입 목록은 이 표에서 쓸 것만 골라 만듭니다.