GW LABS

파이썬 병렬처리: 멀티프로세싱으로 CPU 코어를 100% 활용하는 방법 본문

Programming/Python

파이썬 병렬처리: 멀티프로세싱으로 CPU 코어를 100% 활용하는 방법

GeonWoo Kim 2025. 8. 20. 16:39

서론: 파이썬, 느리다는 편견을 깨다

파이썬은 쉽고 빠르게 코드를 작성할 수 있어 많은 사랑을 받고 있지만, "느리다"는 오명을 종종 듣곤 합니다. 이는 파이썬의 GIL(Global Interpreter Lock) 때문인데요. GIL은 한 번에 하나의 스레드만 파이썬 바이트코드를 실행할 수 있도록 허용하는 메커니즘으로, 멀티코어 환경에서 스레드를 여러 개 사용해도 실질적인 병렬 처리가 어렵게 만듭니다.

하지만 데이터 처리, 웹 스크래핑, 대규모 연산과 같은 작업에서는 이러한 한계가 병목 현상을 유발합니다. 그래서 우리는 이 병목 현상을 극복하고 파이썬의 성능을 극대화할 수 있는 병렬처리 기법에 대해 알아볼 필요가 있습니다. 이번 포스팅에서는 파이썬에서 병렬 처리를 구현하는 대표적인 방법인 멀티프로세싱(Multiprocessing)을 중심으로, 멀티쓰레딩(Multithreading)과 비교하고, 나아가 데이터 처리의 필수 라이브러리인 판다스(Pandas)를 병렬 처리하는 실용적인 방법까지 심도 있게 다뤄보겠습니다.


본론: 파이썬 병렬처리, 제대로 이해하기

1. 멀티쓰레딩 vs. 멀티프로세싱: GIL의 한계를 넘어서

파이썬에서 동시성을 다루는 대표적인 두 가지 방법은 멀티쓰레딩멀티프로세싱입니다. 이 둘의 가장 큰 차이점은 GIL의 적용 여부입니다.

  • 멀티쓰레딩 (Multithreading): 하나의 프로세스 내에서 여러 스레드를 생성하여 작업을 분할합니다. 스레드는 메모리를 공유하기 때문에 데이터 접근이 용이하지만, GIL 때문에 CPU를 많이 사용하는 연산 작업(CPU-bound tasks)에서는 진정한 병렬 처리가 불가능합니다. 주로 네트워크 요청이나 파일 입출력과 같은 I/O-bound 작업에 적합합니다.

  • 멀티프로세싱 (Multiprocessing): 여러 개의 독립적인 프로세스를 생성하여 작업을 수행합니다. 각 프로세스는 별도의 메모리 공간을 가지므로 GIL의 영향을 받지 않고, 여러 CPU 코어를 동시에 활용할 수 있습니다. 따라서 대규모 연산이나 복잡한 계산 등 CPU-bound 작업에 매우 효과적입니다.

간단한 예제 코드를 통해 두 기법의 차이를 직관적으로 살펴보겠습니다.

import time
import multiprocessing
import threading

def cpu_bound_task():
    # 간단한 CPU-bound 작업 (루프를 돌며 계산)
    count = 0
    for _ in range(10**8):
        count += 1

def run_with_multiprocessing():
    start = time.time()
    processes = []
    for _ in range(4): # 4개의 프로세스 생성
        p = multiprocessing.Process(target=cpu_bound_task)
        processes.append(p)
        p.start()

    for p in processes:
        p.join() # 프로세스가 종료될 때까지 기다림
    print(f"멀티프로세싱 소요 시간: {time.time() - start:.2f}초")

def run_with_threading():
    start = time.time()
    threads = []
    for _ in range(4): # 4개의 스레드 생성
        t = threading.Thread(target=cpu_bound_task)
        threads.append(t)
        t.start()

    for t in threads:
        t.join() # 스레드가 종료될 때까지 기다림
    print(f"멀티쓰레딩 소요 시간: {time.time() - start:.2f}초")

if __name__ == '__main__':
    run_with_multiprocessing()
    run_with_threading()

이 코드를 실행하면 멀티프로세싱이 멀티쓰레딩보다 훨씬 빠르게 작업을 완료하는 것을 확인할 수 있습니다.

2. 판다스(Pandas) 병렬처리: 대용량 데이터프레임 가공하기

데이터 분석가와 개발자에게 판다스는 필수적인 라이브러리입니다. 하지만 apply()map() 함수로 대용량 데이터프레임을 처리할 때, 단일 코어만 사용해 시간이 오래 걸리는 경우가 많습니다. 이때 pandarallel 라이브러리를 사용하면 판다스의 연산을 간단하게 병렬 처리할 수 있습니다.

먼저 pandarallel을 설치합니다.

pip install pandarallel

사용법은 매우 간단합니다. 초기화 후 기존 apply 함수 자리에 .parallel_apply()를 사용하기만 하면 됩니다.

import pandas as pd
from pandarallel import pandarallel

# pandarallel 초기화
pandarallel.initialize(nb_workers=4) # 사용할 코어 수 지정

# 예제 데이터프레임 생성
df_size = 10**6
df = pd.DataFrame({
    'a': range(df_size),
    'b': range(df_size)
})

# 복잡한 연산 함수 정의
def complex_operation(row):
    return row['a'] * row['b'] + 100

# 병렬 처리로 데이터프레임 연산
start_parallel = time.time()
df['result_parallel'] = df.parallel_apply(complex_operation, axis=1)
print(f"Pandarallel 병렬 처리 소요 시간: {time.time() - start_parallel:.2f}초")

# 일반적인 판다스 apply 연산
start_serial = time.time()
df['result_serial'] = df.apply(complex_operation, axis=1)
print(f"Pandas 일반 처리 소요 시간: {time.time() - start_serial:.2f}초")

대용량 데이터프레임에서 parallel_apply를 사용하면 일반 apply 대비 연산 속도가 크게 향상되는 것을 확인할 수 있습니다.


결론: 파이썬의 무한한 가능성을 열다

파이썬의 GIL은 단일 스레드 환경에서 메모리 관리를 효율적으로 해주는 장점도 있지만, 멀티코어 시대에 성능의 제약이 되는 것도 사실입니다. 하지만 이번 포스팅에서 살펴본 멀티프로세싱pandarallel과 같은 전문 라이브러리를 활용하면 이러한 한계를 극복하고 파이썬의 진정한 병렬 처리 능력을 끌어낼 수 있습니다.

이러한 기법들은 단순한 코드 최적화를 넘어, 여러분이 다루는 대규모 데이터와 복잡한 연산 작업의 처리 시간을 획기적으로 단축시켜 개발 생산성과 시스템 효율을 동시에 높여줄 것입니다. 💻

Comments