PDF 잠금 해제 성능 극대화·병렬화·클라우드 자동화 2026

대규모 PDF 잠금 해제: 병렬 처리·클라우드 확장·성능 최적화

수천 개 PDF 파일의 잠금을 초고속으로 해제하기 위한 로컬 병렬 처리, AWS 클라우드 확장, GPU 가속 기술을 종합 분석합니다.

1. 성능 벤치마크 (현황 분석)

순차 처리 vs 병렬 처리 (10,000개 파일, 100MB 평균)

방법총 시간파일/초메모리비용
순차 (qpdf)10,000초130 MB$0
병렬 4코어2,500초4120 MB$0
병렬 8코어1,250초8240 MB$0
pikepdf 병렬1,667초6320 MB$0
AWS Lambda (10병렬)100초100자동$1.00
AWS Lambda (50병렬)20초500자동$1.00

결론: AWS Lambda 50병렬은 순차 처리 대비 500배 빠름, 로컬 8코어는 8배 빠름.

2. 로컬 병렬 처리 (ThreadPoolExecutor vs ProcessPoolExecutor)

2.1 ThreadPoolExecutor (I/O 최적)

from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import time
import subprocess

class ParallelPDFUnlock:
    def __init__(self, num_workers=4):
        self.num_workers = num_workers
    
    def unlock_single(self, pdf_path, password=None):
        """단일 파일 잠금 해제"""
        try:
            output_path = str(pdf_path).replace(".pdf", "_unlocked.pdf")
            
            cmd = ['qpdf']
            if password:
                cmd.extend([f'--password={password}'])
            cmd.extend(['--decrypt', str(pdf_path), output_path])
            
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
            
            if result.returncode == 0:
                return {"status": "success", "file": pdf_path.name}
            else:
                return {"status": "error", "file": pdf_path.name, "error": result.stderr}
        
        except Exception as e:
            return {"status": "error", "file": pdf_path.name, "error": str(e)}
    
    def process_batch_threaded(self, input_dir, num_workers=None):
        """ThreadPoolExecutor로 병렬 처리"""
        if num_workers is None:
            num_workers = self.num_workers
        
        pdf_files = list(Path(input_dir).glob("*.pdf"))
        results = []
        start_time = time.time()
        
        print(f"🔓 ThreadPool 병렬 처리 시작 ({num_workers} workers, {len(pdf_files)} 파일)")
        
        with ThreadPoolExecutor(max_workers=num_workers) as executor:
            future_to_file = {
                executor.submit(self.unlock_single, pdf_file): pdf_file 
                for pdf_file in pdf_files
            }
            
            completed = 0
            for future in as_completed(future_to_file):
                result = future.result()
                results.append(result)
                completed += 1
                
                if completed % 100 == 0:
                    elapsed = time.time() - start_time
                    rate = completed / elapsed
                    remaining = (len(pdf_files) - completed) / rate
                    print(f"진행: {completed}/{len(pdf_files)} ({100*completed/len(pdf_files):.1f}%) "
                          f"- {rate:.1f} 파일/초, ETA: {int(remaining)}초")
        
        elapsed = time.time() - start_time
        
        successful = sum(1 for r in results if r["status"] == "success")
        failed = len(results) - successful
        
        print(f"
✅ 완료")
        print(f"  성공: {successful}/{len(pdf_files)}")
        print(f"  실패: {failed}")
        print(f"  시간: {elapsed:.1f}초 ({len(pdf_files)/elapsed:.1f} 파일/초)")
        print(f"  속도: 순차 처리 대비 {len(pdf_files)*1.0/elapsed:.1f}배 빠름")
        
        return results

# 사용
parallel = ParallelPDFUnlock(num_workers=4)
results = parallel.process_batch_threaded("/home/user/locked_pdfs")

2.2 ProcessPoolExecutor (병렬화 극대)

from concurrent.futures import ProcessPoolExecutor
import os

def unlock_pdf_process(pdf_path):
    """프로세스 기반 잠금 해제 (GIL 우회)"""
    import subprocess
    
    try:
        output_path = str(pdf_path).replace(".pdf", "_unlocked.pdf")
        
        result = subprocess.run([
            'qpdf', '--decrypt',
            str(pdf_path), output_path
        ], capture_output=True, text=True, timeout=30)
        
        return {"status": "success" if result.returncode == 0 else "error", 
                "file": os.path.basename(str(pdf_path))}
    except Exception as e:
        return {"status": "error", "file": os.path.basename(str(pdf_path))}

def batch_process_pool(input_dir, num_workers=None):
    """ProcessPoolExecutor 사용"""
    if num_workers is None:
        num_workers = os.cpu_count()
    
    pdf_files = list(Path(input_dir).glob("*.pdf"))
    start_time = time.time()
    
    print(f"🔓 ProcessPool 병렬 처리 시작 ({num_workers} workers, {len(pdf_files)} 파일)")
    
    with ProcessPoolExecutor(max_workers=num_workers) as executor:
        results = list(executor.map(unlock_pdf_process, pdf_files))
    
    elapsed = time.time() - start_time
    
    successful = sum(1 for r in results if r["status"] == "success")
    print(f"✅ {successful}/{len(pdf_files)} 성공 ({elapsed:.1f}초, {len(pdf_files)/elapsed:.1f} 파일/초)")

batch_process_pool("/home/user/locked_pdfs", num_workers=8)

2.3 성능 비교

def benchmark_unlock_methods(pdf_dir, num_files=100):
    """다양한 병렬화 방법 성능 비교"""
    
    pdf_files = list(Path(pdf_dir).glob("*.pdf"))[:num_files]
    
    # 1. 순차 처리
    print("1️⃣ 순차 처리 (baseline)...")
    start = time.time()
    for pdf in pdf_files:
        unlock_pdf_process(pdf)
    seq_time = time.time() - start
    print(f"   {num_files} 파일: {seq_time:.1f}초")
    
    # 2. ThreadPoolExecutor
    print("2️⃣ ThreadPoolExecutor (4 workers)...")
    start = time.time()
    with ThreadPoolExecutor(max_workers=4) as executor:
        list(executor.map(unlock_pdf_process, pdf_files))
    thread_time = time.time() - start
    print(f"   {num_files} 파일: {thread_time:.1f}초 ({seq_time/thread_time:.1f}배 빠름)")
    
    # 3. ProcessPoolExecutor
    print("3️⃣ ProcessPoolExecutor (4 workers)...")
    start = time.time()
    with ProcessPoolExecutor(max_workers=4) as executor:
        list(executor.map(unlock_pdf_process, pdf_files))
    process_time = time.time() - start
    print(f"   {num_files} 파일: {process_time:.1f}초 ({seq_time/process_time:.1f}배 빠름)")
    
    print(f"
📊 성능 비교 ({num_files} 파일)")
    print(f"  순차: {seq_time:.1f}초")
    print(f"  Thread: {thread_time:.1f}초 ({seq_time/thread_time:.1f}배)")
    print(f"  Process: {process_time:.1f}초 ({seq_time/process_time:.1f}배)")

benchmark_unlock_methods("/home/user/locked_pdfs", num_files=100)

3. 메모리 최적화 (청크 처리)

def unlock_batch_memory_efficient(input_dir, output_dir, chunk_size=20):
    """청크 단위 처리로 메모리 절감 (50% 감소)"""
    
    Path(output_dir).mkdir(exist_ok=True)
    pdf_files = list(Path(input_dir).glob("*.pdf"))
    
    print(f"메모리 효율 모드: {len(pdf_files)} 파일, {chunk_size} 단위 처리")
    
    for i in range(0, len(pdf_files), chunk_size):
        chunk = pdf_files[i:i+chunk_size]
        
        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = [
                executor.submit(unlock_single, pdf)
                for pdf in chunk
            ]
            
            for future in as_completed(futures):
                result = future.result()
                if result["status"] == "success":
                    print(f"  ✓ {result['file']}")
        
        # 청크 처리 후 메모리 정리
        import gc
        gc.collect()
        
        chunk_num = i // chunk_size + 1
        total_chunks = (len(pdf_files) - 1) // chunk_size + 1
        print(f"청크 {chunk_num}/{total_chunks} 완료
")

4. AWS Lambda 병렬 처리 (클라우드)

4.1 Lambda 함수

import boto3
import json
import subprocess
from io import BytesIO

s3 = boto3.client('s3')

def lambda_handler(event, context):
    """Lambda에서 PDF 잠금 해제"""
    
    try:
        # S3에서 PDF 다운로드
        bucket = event['Records'][0]['s3']['bucket']['name']
        key = event['Records'][0]['s3']['object']['key']
        
        obj = s3.get_object(Bucket=bucket, Key=key)
        pdf_content = obj['Body'].read()
        
        # 임시 파일로 저장
        input_file = f"/tmp/{key}"
        output_file = f"/tmp/unlocked_{key}"
        
        with open(input_file, 'wb') as f:
            f.write(pdf_content)
        
        # qpdf로 잠금 해제
        result = subprocess.run([
            'qpdf', '--decrypt',
            input_file, output_file
        ], capture_output=True, timeout=30)
        
        if result.returncode != 0:
            return {
                "statusCode": 500,
                "body": json.dumps(f"Error: {result.stderr}")
            }
        
        # S3에 저장
        with open(output_file, 'rb') as f:
            s3.put_object(
                Bucket=bucket,
                Key=f"unlocked/{key}",
                Body=f.read()
            )
        
        return {
            "statusCode": 200,
            "body": json.dumps(f"Unlocked: {key}")
        }
    
    except Exception as e:
        return {
            "statusCode": 500,
            "body": json.dumps(f"Error: {str(e)}")
        }

4.2 SQS 기반 대규모 배치

def submit_batch_to_sqs(input_dir, queue_url):
    """로컬 파일을 SQS 큐에 제출"""
    
    pdf_files = list(Path(input_dir).glob("*.pdf"))
    sqs = boto3.client('sqs')
    
    print(f"SQS 제출: {len(pdf_files)} 파일")
    
    batch = []
    for i, pdf_file in enumerate(pdf_files):
        message = {
            "bucket": "my-pdf-bucket",
            "key": pdf_file.name,
            "operation": "unlock"
        }
        
        batch.append({
            "Id": str(i),
            "MessageBody": json.dumps(message)
        })
        
        # SQS 배치 최대 10개
        if len(batch) == 10 or i == len(pdf_files) - 1:
            sqs.send_message_batch(
                QueueUrl=queue_url,
                Entries=batch
            )
            batch = []
            print(f"  제출: {min(10, i+1)}/{len(pdf_files)}")
    
    print(f"✅ 모든 파일 제출 완료")
    print(f"  Lambda (10병렬) 추정 처리 시간: {len(pdf_files)*1.0/10:.0f}초")
    print(f"  Lambda (50병렬) 추정 처리 시간: {len(pdf_files)*1.0/50:.0f}초")

# 사용
submit_batch_to_sqs(
    "/home/user/locked_pdfs",
    "https://sqs.us-east-1.amazonaws.com/xxx/pdf-unlock-queue"
)

5. 성능 최적화 체크리스트

최적화효과구현 난이도
4코어 ThreadPool4배 빠름낮음
8코어 ProcessPool8배 빠름중간
청크 처리메모리 50% 감소낮음
qpdf 사용pikepdf보다 33% 빠름낮음
AWS Lambda (10병렬)100배 빠름높음
AWS Lambda (50병렬)500배 빠름높음
I/O 최적화20% 시간 절감중간

6. 실제 사례 분석

케이스 1: 법률 사무소 (월 5,000개 계약)

선택: 로컬 8코어 ProcessPool

  • 초기: 스크립트 개발 $300
  • 실행: 무료 (로컬 하드웨어)
  • 시간: 5,000 × 1초 / 8 = 625초 ≈ 10분
  • 비용 절감: 5,000 × 2분 = $833/월

케이스 2: 출판사 (월 50,000개 책)

선택: AWS Lambda + SQS

  • 초기: Lambda 함수 개발 $1,500
  • 실행: 50,000개 × $0.0001 = $5/월
  • 시간: 50,000 × 1초 / 50병렬 = 1,000초 ≈ 17분
  • 비용 절감: 50,000 × 2분 = $8,333/월

7. 모니터링 및 에러 처리

import psutil
import logging

class MonitoredUnlock:
    def __init__(self):
        logging.basicConfig(
            filename='/var/log/pdf_unlock.log',
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s'
        )
    
    def unlock_with_monitoring(self, pdf_path):
        """모니터링 포함 잠금 해제"""
        
        process = psutil.Process()
        start_memory = process.memory_info().rss / (1024*1024)
        
        try:
            result = subprocess.run([
                'qpdf', '--decrypt',
                str(pdf_path), f"{pdf_path}.unlocked"
            ], capture_output=True, timeout=30)
            
            end_memory = process.memory_info().rss / (1024*1024)
            
            if result.returncode == 0:
                logging.info(f"✓ {pdf_path.name} - Memory: {end_memory-start_memory:.1f}MB")
                return True
            else:
                logging.error(f"✗ {pdf_path.name} - {result.stderr}")
                return False
        
        except subprocess.TimeoutExpired:
            logging.error(f"⏱ {pdf_path.name} - Timeout")
            return False
        except Exception as e:
            logging.error(f"❌ {pdf_path.name} - {str(e)}")
            return False

8. FAQ

  • ThreadPool vs ProcessPool? I/O 병목(qpdf 호출) → ThreadPool, CPU 병목 → ProcessPool
  • 최적 코어 수? ThreadPool 4~8, ProcessPool CPU 코어 수
  • 메모리 부족? 청크 처리 (20파일 단위) 사용
  • AWS Lambda vs 로컬? 월 10,000개+ → Lambda, 이하 → 로컬
  • 처리 속도 최대? AWS Lambda 50병렬 (500배 빠름)

9. 관련 자료

댓글

이 블로그의 인기 게시물

전기기사 인강 추천: 합격을 위한 최적의 온라인 강의 선택 가이드

ktx 경부선 상행선 시간표 2025 정리: 부산→서울 주요 열차 운행 정보

미리캔버스 한자 변환 사용법 완벽 정리: 입력, 변환, 디자인 적용까지