PDF 페이지 삭제 성능 최적화 - 속도와 효율성을 위한 고급 기법
PDF 페이지 삭제 성능 최적화 가이드
1,000개의 100 MB PDF를 삭제하는 데 몇 시간이 걸릴 수 있습니다. 하지만 올바른 도구 선택, 시스템 최적화, 병렬 처리, 알고리즘 개선을 통해 처리 시간을 95%까지 단축할 수 있습니다. 이 가이드는 성능 벤치마크, 최적화 기법, 병목 지점 분석을 포함합니다.
성능 벤치마크 비교
도구별 처리 속도 (100 MB, 100페이지 기준)
| 도구 | 처리 시간 | 메모리 사용 | CPU 사용률 | 가성비 |
|---|---|---|---|---|
| PDFKit (웹) | 5~10 s | 50 MB | 10% | ⭐⭐⭐⭐⭐ |
| PDFtk | 2~3 s | 100 MB | 80% | ⭐⭐⭐⭐⭐ |
| Ghostscript | 4~6 s | 200 MB | 60% | ⭐⭐⭐⭐ |
| PyPDF2 | 8~12 s | 300 MB | 50% | ⭐⭐⭐ |
| Adobe Acrobat Pro | 10~15 s | 500 MB | 100% | ⭐⭐ |
| qpdf | 3~4 s | 80 MB | 75% | ⭐⭐⭐⭐⭐ |
병렬 처리 속도 향상
| 워커 수 | 처리 시간 (1,000개 파일) | 처리 속도 | 메모리 (GB) |
|---|---|---|---|
| 순차 (1 워커) | 1,000~2,000초 | 0.5~1 파일/초 | 0.1 |
| 4 워커 (멀티스레드) | 250~500초 | 2~4 파일/초 | 0.4 |
| 8 워커 (멀티코어) | 125~250초 | 4~8 파일/초 | 0.8 |
| 16 워커 (분산) | 62~125초 | 8~16 파일/초 | 1.6 |
병목 지점 분석
| 병목 | 원인 | 영향 | 해결책 |
|---|---|---|---|
| 파일 I/O | 디스크 읽기/쓰기 느림 | 50~60% | SSD 사용, 메모리 디스크 |
| CPU 처리 | PDF 파싱, 인코딩 | 20~30% | 멀티코어 활용 |
| 메모리 | 대용량 파일 로딩 | 10~15% | 스트리밍, 청크 처리 |
| 네트워크 | 웹 도구 업/다운로드 | 20~40% | 로컬 도구, 배치 업로드 |
최적화 기법 1: 도구 선택 최적화
시나리오별 최적 도구
| 상황 | 최적 도구 | 이유 | 예상 속도 |
|---|---|---|---|
| 단일 파일, 즉시 필요 | PDFKit | 설치 불필요, 편리 | 5~10 s |
| 배치 처리 (100~1,000개) | PDFtk + Bash | 가장 빠름, 자동화 용이 | 0.5~1 s/파일 |
| 복잡한 조건 | Python + PyPDF2 | 유연성, 커스터마이징 | 1~2 s/파일 |
| 대규모 (10,000+) | 멀티프로세싱 + qpdf | 최고 성능 | 0.1~0.3 s/파일 |
도구별 기본 명령 (최적화)
# PDFtk (가장 빠름) pdftk input.pdf cat 1-30 output output.pdf # ~2초qpdf (두 번째로 빠름, 메모리 효율)
qpdf --empty --pages input.pdf 1-30 -- output.pdf # ~3초
Ghostscript (품질 중시)
gs -sDEVICE=pdfwrite -dNOPAUSE -dBATCH -sOutputFile=output.pdf input.pdf # ~5초
최적화 기법 2: I/O 성능 향상
디스크 유형별 성능
| 디스크 유형 | 읽기 속도 | 처리 시간 (100 MB) | 비용 |
|---|---|---|---|
| HDD (7,200 RPM) | ~100 MB/s | ~3~5초 | $50 |
| SSD (SATA) | ~500 MB/s | ~1초 | $100 |
| NVMe SSD | ~3,500 MB/s | ~0.1초 | $150 |
| 메모리 디스크 (tmpfs) | ~10,000 MB/s | ~0.05초 | RAM 비용 |
메모리 디스크 활용 (Linux)
#!/bin/bash # 2GB 메모리 디스크 생성 mkdir -p /mnt/ramdisk mount -t tmpfs -o size=2G tmpfs /mnt/ramdisk파일 복사
cp input.pdf /mnt/ramdisk/
처리 (메모리에서 직접)
pdftk /mnt/ramdisk/input.pdf cat 1-30 output /mnt/ramdisk/output.pdf
결과 복사
cp /mnt/ramdisk/output.pdf ./
정리
umount /mnt/ramdisk
성능 향상: HDD 대비 5~10배, SSD 대비 2~3배
최적화 기법 3: 병렬 처리 최적화
멀티스레드 vs 멀티프로세싱
| 방식 | 오버헤드 | 메모리 | GIL 제약 | 권장 용도 |
|---|---|---|---|---|
| 순차 | 없음 | 최소 | 해당 없음 | I/O 대기 시간 있을 때 |
| 멀티스레드 | 낮음 | 낮음 | Python은 CPU 작업 제약 | I/O 바운드 작업 |
| 멀티프로세싱 | 높음 | 높음 | 없음 | CPU 집약적 작업 |
최적 워커 수 계산
#!/bin/bash # CPU 코어 수 확인 CORES=$(nproc) echo "CPU 코어: $CORES"권장 워커 수
I/O 바운드: CORES + 2 (CPU 대기 시간 활용)
CPU 바운드: CORES
메모리 제약: CORES * 0.75
WORKERS=$((CORES + 2)) echo "권장 워커 수: $WORKERS"
배치 처리
python batch_process.py --workers=$WORKERS
파이썬 멀티프로세싱 최적화
import os import multiprocessing as mp from multiprocessing import Pool, cpu_count import subprocess import timedef process_pdf_optimized(args): """최적화된 PDF 처리""" pdf_path, output_path, pages = args cmd = ['pdftk', pdf_path, 'cat', pages, 'output', output_path] subprocess.run(cmd, capture_output=True, timeout=60) return os.path.basename(pdf_path)
def batch_process_optimized(input_dir, output_dir, num_workers=None): """멀티프로세싱 최적화""" if num_workers is None: # 최적 워커 수: CPU 코어 수 num_workers = cpu_count()
pdf_files = [f for f in os.listdir(input_dir) if f.endswith('.pdf')] total = len(pdf_files) # 작업 인자 준비 args_list = [ (os.path.join(input_dir, pdf), os.path.join(output_dir, pdf), '1-30') for pdf in pdf_files ] start_time = time.time() # ctx='spawn': 더 안정적 (Windows/Mac 호환) # ctx='fork': 더 빠름 (Linux만) with mp.get_context('spawn').Pool(processes=num_workers) as pool: results = pool.imap_unordered(process_pdf_optimized, args_list) for idx, result in enumerate(results, 1): elapsed = time.time() - start_time rate = idx / elapsed if elapsed > 0 else 0 remaining = (total - idx) / rate if rate > 0 else 0 print(f"[{idx}/{total}] {result} " f"({rate:.2f} 파일/초, 남은시간: {remaining:.0f}초)") total_time = time.time() - start_time print(f" 완료: {total_time:.2f}초, {total/total_time:.2f} 파일/초")사용
batch_process_optimized('./input', './output')
최적화 기법 4: 메모리 효율화
메모리 프로파일링
import psutil import os from PyPDF2 import PdfReaderdef memory_efficient_processing(pdf_path): """메모리 효율적인 처리"""
# 처리 전 메모리 process = psutil.Process(os.getpid()) mem_before = process.memory_info().rss / 1024 / 1024 # MB # PDF 처리 reader = PdfReader(pdf_path) # 처리 중 메모리 mem_during = process.memory_info().rss / 1024 / 1024 # 페이지 순회 (메모리 누적 방지) for page_num in range(0, len(reader.pages)): page = reader.pages[page_num] # 즉시 처리 후 삭제 (메모리 해제) process_page(page) del page # 명시적 메모리 해제 # 처리 후 메모리 mem_after = process.memory_info().rss / 1024 / 1024 print(f"메모리 변화: {mem_before:.1f} MB → {mem_during:.1f} MB → {mem_after:.1f} MB") print(f"최대 메모리: {mem_during - mem_before:.1f} MB")
def process_page(page): """페이지 처리""" text = page.extract_text() # 처리 로직 pass
최적화 기법 5: 네트워크 최적화
웹 도구 사용 시 최적화
| 최적화 | 방법 | 효과 |
|---|---|---|
| 배치 업로드 | 여러 파일 한 번에 업로드 | 업로드 시간 50% 감소 |
| 네트워크 압축 | gzip 압축 전송 | 대역폭 60~70% 절감 |
| CDN 활용 | 지역별 CDN 서버 | 다운로드 시간 30~50% 감소 |
| 병렬 다운로드 | 여러 커넥션 동시 사용 | 다운로드 시간 3~4배 단축 |
병렬 다운로드 구현
import requests from concurrent.futures import ThreadPoolExecutor import timedef parallel_download(file_urls, num_workers=4): """병렬 다운로드""" start_time = time.time()
with ThreadPoolExecutor(max_workers=num_workers) as executor: futures = {executor.submit(requests.get, url): url for url in file_urls} for idx, future in enumerate(futures, 1): response = future.result() url = futures[future] print(f"[{idx}/{len(file_urls)}] 다운로드: {url} " f"({len(response.content)/1024/1024:.1f} MB)") elapsed = time.time() - start_time total_size = sum(len(requests.get(url).content) for url in file_urls) / 1024 / 1024 print(f"완료: {elapsed:.2f}초, {total_size/elapsed:.2f} MB/초")사용
file_urls = ['https://example.com/pdf1.pdf', 'https://example.com/pdf2.pdf'] parallel_download(file_urls, num_workers=4)
최적화 기법 6: 캐싱 활용
PDF 메타데이터 캐싱
import json from pathlib import Pathdef get_pdf_pages_cached(pdf_path, cache_dir='.pdf_cache'): """페이지 수 캐싱으로 반복 계산 방지""" cache_dir = Path(cache_dir) cache_dir.mkdir(exist_ok=True)
cache_file = cache_dir / f"{Path(pdf_path).stem}.json" # 캐시 확인 if cache_file.exists(): with open(cache_file) as f: cached = json.load(f) if cached['mtime'] == Path(pdf_path).stat().st_mtime: return cached['pages'] # 캐시 없으면 계산 후 저장 from PyPDF2 import PdfReader reader = PdfReader(pdf_path) pages = len(reader.pages) with open(cache_file, 'w') as f: json.dump({ 'pages': pages, 'mtime': Path(pdf_path).stat().st_mtime }, f) return pages사용 (첫 실행: 계산, 두 번째 이후: 캐시)
pages1 = get_pdf_pages_cached('large.pdf') # ~2초 pages2 = get_pdf_pages_cached('large.pdf') # ~0.001초
성능 벤치마크 - 실제 사례
1,000개 파일 (각 100 MB) 처리
| 방식 | 처리 시간 | 처리 속도 | 총 비용 |
|---|---|---|---|
| 웹 도구 순차 | 2~3시간 | 0.1 파일/초 | $0 (무료) |
| PDFtk 순차 | 30~40분 | 0.5 파일/초 | $0 |
| 멀티스레드 (4) | 8~10분 | 2 파일/초 | $0 |
| 멀티프로세싱 (8) | 4~5분 | 4 파일/초 | $0 |
| 분산 (16코어) | 2~3분 | 8 파일/초 | $50 (클라우드) |
최적화 체크리스트
- ✓ 도구 선택 최적화 (PDFtk > qpdf > Ghostscript)
- ✓ 하드웨어 최적화 (SSD 또는 NVMe 사용)
- ✓ 병렬 처리 (CPU 코어 수만큼 워커)
- ✓ 메모리 효율화 (스트리밍, 즉시 삭제)
- ✓ 캐싱 활용 (메타데이터)
- ✓ 네트워크 최적화 (로컬 처리 우선)
- ✓ 배치 크기 조정 (청크 처리)
- ✓ 모니터링 (로그, 진행률)
FAQ
- Q: 가장 빠른 조합은? A: qpdf + 멀티프로세싱 + NVMe = 0.1~0.2 s/파일
- Q: 메모리 부족하면? A: 스트리밍 처리 또는 qpdf 사용 (메모리 효율)
- Q: 네트워크 느리면? A: 로컬 도구 사용, 배치 업로드
- Q: 정확성 vs 속도? A: PDFtk/qpdf는 둘 다 우수, Ghostscript는 품질 우수
댓글
댓글 쓰기