
파이썬으로 고성능 애플리케이션을 개발할 때, 우리는 필연적으로 '병렬성(Parallelism)'과 '동시성(Concurrency)'이라는 벽에 부딪힙니다. 특히 concurrent.futures 모듈에서 제공하는 ProcessPoolExecutor와 ThreadPoolExecutor는 이를 해결하기 위한 가장 강력한 도구입니다. 하지만 많은 개발자가 이 두 실행기의 외형적 사용법에만 집중할 뿐, 내부적으로 데이터가 어떻게 이동하는지, 즉 IPC(Inter-Process Communication)와 메모리 공유 모델에 대해서는 간과하곤 합니다. 내부 통신 방식을 제대로 이해하지 못하면 대용량 데이터를 처리할 때 예상치 못한 성능 저하나 PicklingError 같은 런타임 오류를 해결하는 데 어려움을 겪게 됩니다. 본 가이드에서는 전문가의 시각에서 두 실행기의 하부 구조를 심층 분석하고, 상황에 맞는 최적의 선택 기준을 제시합니다.
1. ThreadPoolExecutor: 공유 메모리를 통한 경량 통신
ThreadPoolExecutor는 동일한 프로세스 내에서 여러 스레드를 생성하여 작업을 수행합니다. 스레드들은 프로세스의 힙(Heap) 메모리 영역을 공유하기 때문에 별도의 복잡한 IPC 메커니즘이 필요하지 않습니다.
- 통신 방식: 메모리 직접 참조 (Shared Memory)
- 장점: 데이터를 복사할 필요가 없어 오버헤드가 매우 적고 빠릅니다.
- 단점: 파이썬의 GIL(Global Interpreter Lock)로 인해 CPU 연산 집약적인 작업에서는 병렬 처리의 이점을 얻기 어렵습니다.
2. ProcessPoolExecutor: 객체 직렬화와 IPC의 결합
ProcessPoolExecutor는 완전히 독립된 개별 프로세스를 생성합니다. 각 프로세스는 자신만의 메모리 공간을 가지므로, 데이터를 주고받기 위해서는 운영체제 수준의 통신이 필요합니다. 파이썬은 이를 위해 multiprocessing 모듈의 Queue와 Pipe를 내부적으로 사용하며, 객체를 전송하기 위해 pickle을 통한 직렬화 과정을 거칩니다.
- 통신 방식: Pickle 기반 직렬화 후 OS Pipe/Socket을 통한 전송
- 장점: GIL의 제약을 받지 않아 멀티코어 CPU 자원을 100% 활용할 수 있습니다.
- 단점: 객체를 직렬화하고 역직렬화하는 과정에서 상당한 CPU 연산과 시간 비용(Overhead)이 발생합니다.
3. 한눈에 비교하는 Executor 내부 작동 메커니즘
두 방식의 구조적 차이를 표를 통해 명확히 비교해 보겠습니다.
| 비교 항목 | ThreadPoolExecutor | ProcessPoolExecutor |
|---|---|---|
| 기본 단위 | Thread (경량) | Process (중량) |
| 주요 통신 방식 | 메모리 주소 직접 공유 | IPC (Pipe, Queue, Pickle) |
| 데이터 전달 비용 | 거의 없음 (Zero-copy에 가까움) | 높음 (직렬화 오버헤드 존재) |
| GIL 영향 | 직접적인 영향 받음 | 영향 없음 (회피 가능) |
| 주요 타겟 작업 | I/O Bound (네트워크, 파일 읽기) | CPU Bound (연산, 이미지 처리) |
| 안정성 | 하나의 스레드 오류가 전체 영향 가능 | 프로세스 격리로 상대적 안전 |
4. 실전 예제: 상황별 선택 방법 (Sample Example)
다음은 대량의 숫자 리스트를 처리할 때 각 Executor가 어떻게 동작하는지 보여주는 예시 코드입니다.
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def heavy_computation(n):
# CPU 집약적 작업 예시
return sum(i * i for i in range(n))
data = [1000000 + i for i in range(10)]
# 1. ThreadPoolExecutor (I/O에 유리, 여기서는 비효율적)
start = time.time()
with ThreadPoolExecutor() as executor:
results = list(executor.map(heavy_computation, data))
print(f"Thread Time: {time.time() - start:.4f}s")
# 2. ProcessPoolExecutor (CPU 연산에 유리, 직렬화 발생)
start = time.time()
with ProcessPoolExecutor() as executor:
results = list(executor.map(heavy_computation, data))
print(f"Process Time: {time.time() - start:.4f}s")
전문가 팁: 데이터의 크기가 수 기가바이트(GB)에 달한다면 ProcessPoolExecutor의 IPC 비용이 연산 이득을 넘어설 수 있습니다. 이럴 때는 multiprocessing.shared_memory를 검토하는 것이 해결 방법입니다.
5. 결론 및 최적화 전략
파이썬 병렬 처리의 핵심은 "통신 비용이 연산 이득보다 작은가?"를 판단하는 것입니다. ThreadPoolExecutor는 통신 비용이 0에 가깝지만 연산 병렬화가 안 되고, ProcessPoolExecutor는 진정한 병렬 연산이 가능하지만 데이터 전달 과정이 무겁습니다. 따라서 단순 I/O 대기 시간이 많은 작업은 스레드를, 복잡한 수치 계산이나 데이터 파싱은 프로세스를 선택하십시오. 만약 프로세스 간 통신에서 병목이 발생한다면, 전달하는 객체의 크기를 최소화하거나 기본 자료형(Primitive types) 위주로 구성하여 직렬화 속도를 높이는 것이 성능 해결의 열쇠입니다.