안녕하세요! 지난 시간에는 파이썬의 강력한 문법인 데코레이터(Decorator)에 대해 알아보았습니다. 이제 여러분은 함수를 직접 수정하지 않고도 기능을 확장하는 우아한 방법을 이해하셨을 거예요!
이번 시간에는 파이썬에서 여러 작업을 동시에 처리하여 프로그램의 효율성을 높이는 두 가지 중요한 개념인 **멀티스레딩(Multithreading)**과 **멀티프로세싱(Multiprocessing)**에 대해 알아보겠습니다.
단일 코어 CPU에서는 한 번에 하나의 작업만 실행할 수 있지만, 멀티스레딩과 멀티프로세싱은 이러한 제약을 넘어 여러 작업을 '병렬적으로' 또는 '동시에' 실행되는 것처럼 보이게 하거나 실제로 병렬로 실행하여 프로그램의 응답성을 높이거나 처리 시간을 단축시킬 수 있습니다.
마치 하나의 요리사(CPU)가 여러 요리(작업)를 동시에 진행하는(멀티스레딩) 것과, 여러 요리사(CPU 코어)가 각자 다른 요리(작업)를 동시에 진행하는(멀티프로세싱) 것에 비유할 수 있습니다. 그럼, 이 두 가지 개념이 어떻게 작동하는지 함께 살펴볼까요?
Part 1: 멀티스레딩 (Multithreading) - 동시성(Concurrency) 달성
**스레드(Thread)**는 프로세스 내에서 실행되는 가장 작은 실행 단위입니다. 하나의 프로세스는 여러 스레드를 가질 수 있으며, 이 스레드들은 프로세스의 메모리 공간을 공유합니다. 멀티스레딩은 하나의 프로그램(프로세스) 내에서 여러 스레드를 동시에 실행하는 기법입니다.
1. 파이썬의 GIL (Global Interpreter Lock)
파이썬의 CPython 인터프리터에는 **GIL(Global Interpreter Lock)**이라는 메커니즘이 존재합니다. GIL은 한 번에 하나의 스레드만 파이썬 바이트코드를 실행할 수 있도록 강제하는 락(Lock)입니다.
- 영향: GIL 때문에 파이썬의 멀티스레딩은 **CPU-Bound 작업(CPU 연산이 많은 작업)**에서는 진정한 병렬 처리(Parallelism)를 달성하기 어렵습니다. 즉, 여러 스레드가 동시에 CPU를 사용하여 계산하는 것이 불가능합니다.
- 활용: 하지만 **I/O-Bound 작업(입출력 작업이 많은 작업)**에서는 여전히 유용합니다. 파일 읽기/쓰기, 네트워크 통신(웹 요청), 데이터베이스 쿼리 등 I/O 작업이 진행되는 동안 GIL은 해제될 수 있으므로, 다른 스레드가 CPU를 사용하여 다른 작업을 수행할 수 있습니다. 이는 프로그램의 응답성을 높이는 데 도움이 됩니다.
2. 멀티스레딩 예시 (I/O-Bound 시뮬레이션)
간단한 웹 페이지 다운로드를 시뮬레이션하는 예제를 통해 멀티스레딩의 동작을 살펴보겠습니다.
# 파일 이름: multithreading_example.py
import threading
import time
import requests # 웹 요청을 위한 라이브러리 (설치 필요: pip install requests)
# I/O-bound 작업을 시뮬레이션하는 함수
def download_webpage(url):
"""
주어진 URL의 웹 페이지를 다운로드하는 작업을 시뮬레이션합니다.
실제 웹 요청을 보내는 대신, 짧은 시간 지연과 메시지 출력을 사용합니다.
"""
print(f"[{threading.current_thread().name}] {url} 다운로드 시작...")
try:
# 실제 웹 요청 대신 시뮬레이션 (네트워크 지연을 흉내냄)
# response = requests.get(url, timeout=5)
time.sleep(2) # 2초간 네트워크 지연 시뮬레이션
# print(f"[{threading.current_thread().name}] {url} 다운로드 완료. 상태 코드: {response.status_code}")
print(f"[{threading.current_thread().name}] {url} 다운로드 완료.")
except Exception as e:
print(f"[{threading.current_thread().name}] {url} 다운로드 실패: {e}")
def run_multithreading():
"""
멀티스레딩을 사용하여 여러 웹 페이지 다운로드 작업을 실행합니다.
"""
urls = [
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
"https://example.com/page4"
]
threads = []
start_time = time.time()
print("\n--- 멀티스레딩 시작 ---")
for i, url in enumerate(urls):
# 각 URL에 대해 새로운 스레드 생성
thread = threading.Thread(target=download_webpage, args=(url,), name=f"Thread-{i+1}")
threads.append(thread)
thread.start() # 스레드 시작
# 모든 스레드가 종료될 때까지 기다림
for thread in threads:
thread.join()
end_time = time.time()
print(f"--- 멀티스레딩 종료. 총 소요 시간: {end_time - start_time:.2f}초 ---")
if __name__ == "__main__":
run_multithreading()
코드 설명:
- download_webpage 함수는 time.sleep(2)를 사용하여 네트워크 I/O 지연을 시뮬레이션합니다.
- threading.Thread를 사용하여 각 download_webpage 함수를 별도의 스레드에서 실행합니다.
- thread.start()는 스레드를 시작하고, thread.join()은 해당 스레드가 종료될 때까지 메인 스레드가 기다리도록 합니다.
- 이 예제는 각 다운로드 작업이 독립적으로 진행되는 동안 다른 스레드가 시작될 수 있음을 보여주며, 총 소요 시간이 단일 스레드로 각 2초씩 4번 실행하는 것(총 8초)보다 훨씬 짧게 나옵니다. (약 2초대)
[VS Code 터미널 출력]

Part 2: 멀티프로세싱 (Multiprocessing) - 진정한 병렬 처리(Parallelism) 달성
**프로세스(Process)**는 운영체제로부터 독립적인 메모리 공간을 할당받아 실행되는 프로그램의 인스턴스입니다. 각 프로세스는 자체적인 스레드와 자원을 가집니다. 멀티프로세싱은 여러 개의 독립적인 프로세스를 동시에 실행하는 기법입니다.
1. GIL 우회 및 활용
- 각 프로세스는 독립적인 파이썬 인터프리터를 가지므로, GIL의 영향을 받지 않습니다.
- 따라서 멀티프로세싱은 CPU-Bound 작업에서 진정한 병렬 처리(Parallelism)를 달성하여 성능 향상을 기대할 수 있습니다. 다중 코어 CPU의 모든 코어를 효율적으로 활용할 수 있습니다.
- 단점: 프로세스 간 통신은 스레드 간 통신보다 복잡하고 오버헤드가 더 큽니다 (메모리를 공유하지 않기 때문). 또한, 프로세스 생성 비용이 스레드 생성 비용보다 높습니다.
2. 멀티프로세싱 예시 (CPU-Bound 시뮬레이션)
복잡한 계산을 시뮬레이션하는 예제를 통해 멀티프로세싱의 동작을 살펴보겠습니다.
# 파일 이름: multiprocessing_example.py
import multiprocessing
import time
# CPU-bound 작업을 시뮬레이션하는 함수
def cpu_heavy_task(number):
"""
주어진 숫자에 대해 복잡한 계산을 시뮬레이션합니다.
(예: 매우 큰 숫자의 제곱근 계산을 반복)
"""
print(f"[{multiprocessing.current_process().name}] {number} 계산 시작...")
result = 0
for i in range(1, 5_000_000): # 5백만 번 반복하는 무거운 연산
result += (i * number) ** 0.5 # 제곱근 계산
print(f"[{multiprocessing.current_process().name}] {number} 계산 완료. 결과 일부: {result:.2f}")
return result
def run_multiprocessing():
"""
멀티프로세싱을 사용하여 여러 CPU-bound 작업을 실행합니다.
"""
numbers_to_process = [10, 20, 30, 40]
processes = []
start_time = time.time()
print("\n--- 멀티프로세싱 시작 ---")
for i, num in enumerate(numbers_to_process):
# 각 숫자에 대해 새로운 프로세스 생성
process = multiprocessing.Process(target=cpu_heavy_task, args=(num,), name=f"Process-{i+1}")
processes.append(process)
process.start() # 프로세스 시작
# 모든 프로세스가 종료될 때까지 기다림
for process in processes:
process.join()
end_time = time.time()
print(f"--- 멀티프로세싱 종료. 총 소요 시간: {end_time - start_time:.2f}초 ---")
if __name__ == "__main__":
# Windows에서는 multiprocessing 모듈을 사용할 때 if __name__ == "__main__": 블록이 필수적입니다.
# 그렇지 않으면 무한 루프가 발생할 수 있습니다.
run_multiprocessing()
코드 설명:
- cpu_heavy_task 함수는 반복적인 제곱근 계산을 통해 CPU 연산을 많이 소모하도록 시뮬레이션합니다.
- multiprocessing.Process를 사용하여 각 cpu_heavy_task 함수를 별도의 프로세스에서 실행합니다.
- process.start()는 프로세스를 시작하고, process.join()은 해당 프로세스가 종료될 때까지 메인 프로세스가 기다리도록 합니다.
- 이 예제는 CPU 코어 수에 따라 단일 프로세스로 각 작업을 순차적으로 실행하는 것보다 훨씬 빠르게 완료될 수 있습니다. (예: 4코어 CPU에서 각 4초 걸리는 작업 4개를 병렬로 실행하면 총 4초대에 완료될 수 있음)
[VS Code 터미널 출력]

Part 3: 멀티스레딩 vs 멀티프로세싱 비교
두 가지 동시성/병렬성 기법의 주요 차이점을 요약하고, 언제 어떤 것을 선택해야 할지 알아보겠습니다.
특징 | 멀티스레딩 (Multithreading) | 멀티프로세싱 (Multiprocessing) |
실행 단위 | 스레드 (프로세스 내의 경량 실행 단위) | 프로세스 (독립적인 실행 환경) |
메모리 공유 | 동일 프로세스 내에서 메모리 공유 (데이터 공유 용이) | 각 프로세스가 독립적인 메모리 공간 가짐 (데이터 공유 복잡) |
GIL 영향 | GIL의 영향을 받음 (CPU-bound 작업에서 진정한 병렬성 어려움) | GIL의 영향을 받지 않음 (CPU-bound 작업에서 진정한 병렬성 가능) |
생성 비용 | 낮음 (스레드 생성 및 전환 비용이 적음) | 높음 (새로운 프로세스 생성 및 메모리 할당 비용이 큼) |
통신 | 공유 메모리를 통한 직접 통신 (락/뮤텍스 필요) | 파이프(Pipe), 큐(Queue), 공유 메모리 등 복잡한 IPC(Inter-Process Communication) 메커니즘 필요 |
오버헤드 | 낮음 | 높음 |
적합한 경우 | I/O-Bound 작업 (네트워크 통신, 파일 I/O 등) | CPU-Bound 작업 (복잡한 계산, 데이터 처리 등) |
예시 | 웹 서버, GUI 애플리케이션, 웹 크롤러 | 과학 계산, 이미지/비디오 처리, 대규모 데이터 분석 |
실제 성능 비교 예시:
아래 코드는 CPU-Bound 작업과 I/O-Bound 작업을 각각 멀티스레딩과 멀티프로세싱으로 실행하여 GIL의 영향을 시각적으로 보여줍니다.
# 파일 이름: performance_comparison.py
import threading
import multiprocessing
import time
import os
# --- CPU-Bound 작업 시뮬레이션 ---
def cpu_work(n):
"""CPU를 많이 사용하는 작업 시뮬레이션"""
sum_val = 0
for i in range(n):
sum_val += i * i
return sum_val
# --- I/O-Bound 작업 시뮬레이션 ---
def io_work(n):
"""I/O 대기를 시뮬레이션하는 작업"""
time.sleep(n / 1000) # 밀리초 단위로 지연
return f"I/O 작업 {n}ms 완료"
# --- 단일 스레드/프로세스 실행 ---
def run_single(task_func, args_list, task_name):
print(f"\n--- 단일 {task_name} 실행 시작 ---")
start_time = time.time()
results = []
for args in args_list:
results.append(task_func(args))
end_time = time.time()
print(f"--- 단일 {task_name} 실행 종료. 총 소요 시간: {end_time - start_time:.2f}초 ---")
# print(f"결과: {results[:3]}...") # 결과가 너무 길면 일부만 출력
# --- 멀티스레딩 실행 ---
def run_threaded(task_func, args_list, task_name):
print(f"\n--- 멀티스레딩 {task_name} 실행 시작 ---")
threads = []
start_time = time.time()
for args in args_list:
thread = threading.Thread(target=task_func, args=(args,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end_time = time.time()
print(f"--- 멀티스레딩 {task_name} 실행 종료. 총 소요 시간: {end_time - start_time:.2f}초 ---")
# --- 멀티프로세싱 실행 ---
def run_processed(task_func, args_list, task_name):
print(f"\n--- 멀티프로세싱 {task_name} 실행 시작 ---")
processes = []
start_time = time.time()
for args in args_list:
process = multiprocessing.Process(target=task_func, args=(args,))
processes.append(process)
process.start()
for process in processes:
process.join()
end_time = time.time()
print(f"--- 멀티프로세싱 {task_name} 실행 종료. 총 소요 시간: {end_time - start_time:.2f}초 ---")
if __name__ == "__main__":
# CPU-Bound 작업 테스트 설정
CPU_TASK_ITERATIONS = 50_000_000 # 각 작업의 반복 횟수
CPU_TASKS = 4 # 실행할 작업의 개수
cpu_args = [CPU_TASK_ITERATIONS] * CPU_TASKS
# I/O-Bound 작업 테스트 설정
IO_TASK_DELAY_MS = 500 # 각 작업의 지연 시간 (ms)
IO_TASKS = 4 # 실행할 작업의 개수
io_args = [IO_TASK_DELAY_MS] * IO_TASKS
print(f"현재 시스템의 CPU 코어 수: {os.cpu_count()}")
# CPU-Bound 작업 테스트
run_single(cpu_work, cpu_args, "CPU-Bound 작업 (단일)")
run_threaded(cpu_work, cpu_args, "CPU-Bound 작업 (멀티스레딩)")
run_processed(cpu_work, cpu_args, "CPU-Bound 작업 (멀티프로세싱)")
# I/O-Bound 작업 테스트
run_single(io_work, io_args, "I/O-Bound 작업 (단일)")
run_threaded(io_work, io_args, "I/O-Bound 작업 (멀티스레딩)")
run_processed(io_work, io_args, "I/O-Bound 작업 (멀티프로세싱)")
기대 결과 (예시, 시스템 환경에 따라 다름):
- CPU-Bound 작업:
- 단일: 오래 걸림 (예: 15초)
- 멀티스레딩: 단일 스레드와 비슷하거나 약간 더 오래 걸림 (GIL 때문에 병렬성 없음) (예: 15~16초)
- 멀티프로세싱: CPU 코어 수에 비례하여 빨라짐 (예: 4코어에서 4초)
- I/O-Bound 작업:
- 단일: 오래 걸림 (예: 2초 * 4 = 8초)
- 멀티스레딩: I/O 대기 중 GIL이 해제되어 빨라짐 (예: 2초대)
- 멀티프로세싱: 스레딩과 비슷하게 빨라지지만, 프로세스 생성 오버헤드 때문에 스레딩보다 약간 더 오래 걸릴 수 있음 (예: 2.5초)
이 결과를 통해 GIL의 영향과 각 기법의 장단점을 명확히 이해할 수 있습니다.
[VS Code 터미널 출력 (예시, 시스템 환경에 따라 다름)]

마무리하며
이번 시간에는 파이썬에서 여러 작업을 동시에 처리하는 **멀티스레딩(Multithreading)**과 **멀티프로세싱(Multiprocessing)**에 대해 자세히 알아보았습니다.
- 멀티스레딩: GIL 때문에 I/O-Bound 작업에 주로 사용되며, 응답성 향상에 기여합니다.
- 멀티프로세싱: GIL을 우회하여 CPU-Bound 작업에서 진정한 병렬 처리를 가능하게 하며, 다중 코어 CPU를 최대한 활용합니다.
두 기법은 각각의 장단점과 적합한 사용 사례가 있으므로, 여러분의 프로그램이 어떤 종류의 병목 현상을 겪는지 (CPU-Bound인지, I/O-Bound인지) 파악하여 올바른 방법을 선택하는 것이 중요합니다.
이것으로 '파이썬 고급 문법 & 실전 예제' 챕터의 첫 번째 포스팅이 마무리됩니다. 다음 포스팅에서는 파이썬의 패키지와 모듈을 효과적으로 관리하는 **가상 환경(Virtual Environment)**의 개념과 사용법에 대해 알아보겠습니다.
궁금한 점이 있다면 언제든지 질문해주세요! 다음 포스팅에서 만나요!