‘Simple’ QR Code Generator가 내 RAM을 모두 잡아먹다: 5만 개 QR Code 이야기
Source: Dev.to
위에 제공된 Source 링크만으로는 번역할 본문이 포함되어 있지 않습니다. 번역을 원하는 전체 텍스트(마크다운 형식 포함)를 제공해 주시면, 요청하신 대로 한국어로 번역해 드리겠습니다.
무엇이 잘못됐는가
내 초기 스크립트는 모든 QR 코드를 미리 생성하고, 메모리에 모두 캐시한 뒤 PDF를 조합했습니다. 논리적으로 보였죠:
def generate_pdf(output_path: str, total: int = 50000):
ids = generate_unique_ids(total)
# Pre‑generate ALL QR codes in parallel for "speed"
print(f"Pre-generating {total} QR codes in parallel...")
num_workers = cpu_count()
# Split IDs into batches for parallel processing
batch_size = max(1, total // (num_workers * 4))
batches = [ids[i:i + batch_size] for i in range(0, len(ids), batch_size)]
# Generate QR codes in parallel using multiprocessing
qr_cache = {}
with Pool(num_workers) as pool:
results = list(tqdm(
pool.imap(generate_qr_batch, batches),
total=len(batches),
desc="Generating QR codes"
))
# Store ALL images in memory
for batch_result in results:
for uid, img_bytes in batch_result:
buf = io.BytesIO(img_bytes)
qr_cache[uid] = ImageReader(buf)
# NOW create the PDF using cached images
# ... PDF generation code ...
나는 멀티프로세싱, 병렬 실행, 배치 처리 같은 유행어에 자부심을 느꼈습니다.
실행했을 때 진행 바가 움직이고, CPU 사용량이 모든 코어에서 100 %에 달했으며—그 다음—노트북(16 GB RAM)이 점점 버벅이기 시작했습니다:
2 GB...
4 GB...
8 GB...
12 GB...
OOM 킬러가 프로세스를 종료시켰습니다. PDF는 생성되지 않았고, 기계는 멈춰버렸으며 나는 값비싼 교훈을 얻었습니다.
왜 폭발했는가
| 항목 | 대략적인 크기 |
|---|---|
| QR 코드 (400 × 400 px) PNG (압축) | 15‑30 KB |
QR 코드를 PIL.Image 객체로 | ~500 KB – 1 MB |
| 50 000 QR 코드 × 500 KB ≈ | ~25 GB RAM |
| 50 000 PNG 바이트 × 20 KB ≈ | ~1 GB RAM |
+ ImageReader 객체, BytesIO 버퍼, Python 오버헤드, 멀티프로세싱 복제 | 2‑4 GB 관측 |
압축된 바이트조차도 내 RAM을 소진했을 것이며, 병렬 워커가 데이터를 복제하여 사용량이 더욱 증가했습니다.
근본적인 결함: 속도를 최적화하면서 자원 소비를 무시한 것.
간단한 해결책 – 스트림 기반 처리
한 번에 모두 로드하는 대신 한 PDF 페이지씩 (페이지당 30개의 QR 코드) 처리합니다. 현재 페이지의 이미지만 메모리에 보관합니다.
def generate_pdf(output_path: str, total: int = 50000):
ids = generate_unique_ids(total)
total_pages = (total + PER_PAGE - 1) // PER_PAGE
# Create PDF canvas
c = canvas.Canvas(output_path, pagesize=A4)
# Process ONE PAGE at a time
for page_start in tqdm(range(0, total, PER_PAGE), desc="Generating PDF pages"):
page_ids = ids[page_start : page_start + PER_PAGE]
# Generate QR codes ONLY for this page
page_qr_cache = {}
for uid in page_ids:
img = make_qr_image(uid)
page_qr_cache[uid] = img_to_reader(img)
# Draw this page
for idx, uid in enumerate(page_ids):
# ... draw QR code to PDF ...
c.drawImage(page_qr_cache[uid], qr_x, qr_y, ...)
c.showPage()
# CRITICAL: Clear the cache after each page!
page_qr_cache.clear()
c.save()
핵심 변경 사항
- 페이지별 생성 – 언제든 메모리에 30개의 QR 코드만 존재합니다.
- 각 페이지 후 명시적 캐시 정리.
- 멀티프로세싱 제거 – 데이터 중복을 없애고 흐름을 단순화합니다.
Trade‑offs: Original vs. Optimized
| Metric | Original (Parallel) | Optimized (Per‑Page) |
|---|---|---|
| Memory Usage | 2‑4 GB | 50‑100 MB |
| Speed | Faster (theoretically) | Slower (sequential) |
| Stability | Crashes on large datasets | Stable |
| Scalability | Limited by RAM | Limited by disk space |
네, 새 버전은 느립니다. 50 000개의 QR 코드를 순차적으로 생성하는 데 30‑45 분이 걸렸지만, 완료되기 전에 충돌하는 상황보다 훨씬 나은 결과입니다. 속담에 이르듯이 끝까지 실행되는 느린 스크립트가 결코 끝나지 않는 빠른 스크립트보다 무한히 빠릅니다.
요약
- 규모를 생각하라 – 100개의 항목에 작동하는 스크립트가 10 000개에서는 폭발할 수 있다.
- 대량 로드보다 스트리밍을 선호하라 대규모 데이터셋을 다룰 때.
- CPU뿐 아니라 메모리도 측정하라; 병렬 처리는 RAM 사용량을 증가시킬 수 있다.
- 단순함이 종종 최선이다 – 불필요한 복잡성(멀티프로세싱, 대규모 캐시)을 제거하면 스크립트가 견고해질 수 있다.
최적화된 버전을 밤새 실행했다. 일어나 보니 두 개의 PDF 파일(전체 100 000개의 QR 코드)이 모두 준비돼 있었고, 내 컴퓨터는 여전히 원활하게 작동하고 있었다.
*혹시 “모든 것을 미리 계산”하고 싶어질 때가 있다면, 잠시 멈추고 스스로에게 물어보라: **규모가 10×? 100×? 1000×?*
메모리‑폭탄 문제
스크립트를 확장하면 메모리 문제가 치명적으로 변할 수 있습니다. 생성하는 모든 객체는 메모리 어딘가에 존재하며, 이미지 객체는 생각보다 크게 차지할 수 있습니다.
숨겨진 메모리 사용량 증가 사례
# This innocent‑looking line...
qr_cache[uid] = ImageReader(buf)
# ...executed 50,000 times becomes a memory bomb
병렬 처리는 CPU‑집약적 작업에 좋지만, 충분한 메모리가 여러 워커를 지원할 수 있을 때만 유효합니다. 각 워커가 큰 객체를 생성하면 병렬화가 실제로 메모리 사용량을 곱셈하게 만들어 상황을 악화시킵니다. 때로는 간단한 순차 루프가 최선일 수 있습니다.
Tip: 파이썬의 가비지 컬렉터는 도움이 되지만 마법은 아닙니다. 큰 객체에 대한 참조를 사전이나 리스트에 보관하고 있으면, 해당 참조를 명시적으로 제거할 때까지 메모리가 해제되지 않습니다.
# This single line saved gigabytes of RAM
page_qr_cache.clear()
진행 바 사용
장시간 실행되는 작업을 할 때는 항상 진행 바를 추가하세요. tqdm 라이브러리를 사용하면 매우 간단합니다:
from tqdm import tqdm
for page_start in tqdm(
range(0, total, PER_PAGE),
desc="Generating PDF pages"
):
# ... your code ...
진행 바는 작업이 얼마나 걸릴지 피드백을 제공하고, 정체 현상을 쉽게 발견하도록 도와줍니다.
확장하기 전에 물어볼 세 가지 질문
- 각 항목당 메모리 사용량은 얼마인가요?
- 몇 개의 항목을 처리할 예정인가요?
- 모두 한 번에 처리하는 대신 항목을 하나씩 처리할 수 있나요?
이 질문들은 특히 다음 경우에 중요합니다:
- 이미지 처리: 이미지가 메모리를 많이 차지합니다.
- 데이터 파이프라인: 대용량 CSV/JSON 파일.
- API 응답: 수천 개 레코드를 페이지네이션으로 처리.
- 파일 작업: 큰 파일을 읽고 쓰기.
패턴: 가능하면 스트리밍하고, 반드시 배치가 필요할 때는 배치하며, 절대 필요하지 않은 한 모든 데이터를 메모리에 로드하지 마세요.
실용적인 팁
거대한 메모리 내 리스트 구축 피하기
# Bad: creates a list of 50,000 items in memory
ids = [generate_id() for _ in range(50_000)]
# Better: generates one at a time
def id_generator(count):
for _ in range(count):
yield generate_id()
항목을 점진적으로 처리하기
# Instead of processing all at once
for item in huge_list:
process(item)
# Process in manageable chunks
chunk_size = 100
for i in range(0, len(huge_list), chunk_size):
chunk = huge_list[i:i + chunk_size]
for item in chunk:
process(item)
# Clean up after each chunk
import gc
gc.collect() # Force garbage collection if needed
메모리 사용량 모니터링
import psutil, os
def get_memory_usage():
process = psutil.Process(os.getpid())
# Return MB
return process.memory_info().rss / 1024 / 1024
# In your loop
for i, item in enumerate(items):
process(item)
if i % 1_000 == 0:
print(f"Processed {i} items, Memory: {get_memory_usage():.1f} MB")
메모리 제한 설정 (Unix)
import resource
# Limit memory to 1 GB
resource.setrlimit(resource.RLIMIT_AS, (1_024 * 1_024 * 1_024, -1))
요약
내 “간단한” QR‑코드 생성기가 리소스 관리에 대한 귀중한 교훈이 되었습니다. 원래 코드는 교묘했습니다—병렬 처리, 배치 작업, 캐싱—하지만 작동하지 않는 교묘한 코드는 작동하는 간단한 코드보다 더 나쁩니다.
최종 버전:
- 두 개의 PDF 파일에 걸쳐 100 000개의 QR 코드를 생성합니다.
- 실행에 약 1시간 정도 걸립니다.
- … 를 사용합니다.
기억하세요: 메모리를 먼저, 속도를 나중에 생각하세요. 완전히 실행되는 느린 스크립트가 충돌하는 빠른 스크립트보다 훨씬 더 가치 있습니다.
TL;DR
50 000개의 QR 코드를 한 번에 메모리에 모두 로드해서 생성하려고 했습니다. 컴퓨터의 RAM이 부족해 크래시가 발생했습니다.
Fix: QR 코드를 페이지별(예: 한 번에 30개)로 생성하세요. 속도는 느려지지만 정상적으로 동작합니다.
Lesson: 대규모 데이터를 다룰 때는 항상 메모리 사용량을 고려하세요.