파이썬 multiprocessing pickle 오류: 함수와 인자부터 분리해 확인하는 법

파이썬 multiprocessing에서 Can't pickle local object 또는 cannot pickle 오류가 나면 if __name__ == '__main__':만 추가해서는 해결되지 않을 수 있습니다. 먼저 프로세스에 넘긴 target 함수와 args 안의 인자를 따로 직렬화해 어느 쪽이 문제인지 확인해야 하며, 중첩 함수·lambda 대신 모듈 최상위 callable과 단순한 데이터 인자를 쓰는 것이 기본 해법입니다. 이 글에서는 그 판단 순서와 함수, callable 클래스, 스레드 방식 사이의 선택 기준을 정리합니다.

main guard를 넣었는데도 오류가 남는 이유

main guard는 자식 프로세스가 모듈을 다시 가져올 때 프로세스 생성 코드가 반복 실행되는 문제를 막는 진입점 보호 장치입니다. 그러나 target 자체나 전달 인자까지 pickle할 수 있게 바꾸어 주지는 않습니다. multiprocessing RuntimeError와 freeze_support() 적용 기준과는 겹치지 않게, 여기서는 프로세스 생성 진입점보다 직렬화 대상의 구조를 먼저 봅니다.

다음처럼 함수와 인자를 구성했다고 가정해 보겠습니다.

from multiprocessing import Process


def start_worker():
    prefix = "result:"

    def write_result(value):
        print(prefix, value)

    process = Process(target=write_result, args=(10,))
    process.start()
    process.join()


if __name__ == "__main__":
    start_worker()

여기서 write_result는 start_worker 안에 정의된 지역 함수입니다. args의 10이 문제가 아니라 target callable을 자식 프로세스가 import 가능한 모듈의 이름으로 찾기 어렵다는 점이 핵심입니다. lambda도 같은 방향으로 점검해야 합니다. Python의 pickle은 함수의 코드 전체를 전달하는 방식이 아니라 함수가 속한 모듈과 정규 이름을 이용해 복원하므로, 현재 세션이나 지역 스코프에만 있는 callable은 프로세스 경계를 넘길 때 취약합니다. (출처: Python multiprocessing 문서, Python pickle 문서)

target과 args를 따로 검사하는 방법

오류 메시지만 보고 target과 args 중 하나를 단정하지 말고, 프로세스를 시작하기 전에 pickle.dumps()로 각각 시험해 보는 편이 빠릅니다.

import pickle


def check_picklable(name, value):
    try:
        pickle.dumps(value)
    except Exception as exc:
        print(f"{name}: 실패 - {type(exc).__name__}: {exc}")
        return False
    else:
        print(f"{name}: 확인됨")
        return True


def worker(value):
    return value * 2


if __name__ == "__main__":
    target = worker
    args = (10,)

    target_ok = check_picklable("target", target)
    args_ok = check_picklable("args", args)

    if target_ok and args_ok:
        print("프로세스 시작 전 직렬화 항목 확인 완료")

이 검사는 multiprocessing이 내부에서 사용하는 모든 경로를 재현하는 테스트는 아니지만, 문제를 함수 쪽과 데이터 쪽으로 좁히는 실무 점검으로 쓸 수 있습니다. pickle할 수 없는 객체를 직렬화하면 PicklingError가 발생할 수 있고, tuple 안에 문제가 있는 원소가 들어 있으면 전체 args 직렬화가 실패합니다. 따라서 tuple 자체가 아니라 파일 핸들, 락, 열린 소켓, 세션 객체처럼 내부에 포함된 값도 확인해야 합니다. (출처: Python pickle 문서)

함수를 모듈 최상위로 옮기는 기본 수정

가장 단순한 수정은 worker를 파일의 모듈 최상위에 두고, 프로세스 생성은 main guard 안에서 하는 것입니다.

from multiprocessing import Process


def write_result(value, prefix):
    print(prefix, value)


if __name__ == "__main__":
    process = Process(target=write_result, args=(10, "result:"))
    process.start()
    process.join()

이 구조는 실행 대상과 데이터의 경계를 눈에 보이게 합니다. prefix처럼 문자열·숫자·bytes·list·tuple·set·dict 등 일반 자료형으로 표현할 수 있는 값은 내부 원소도 직렬화 가능한지 확인한 뒤 전달합니다. 파일을 읽어야 한다면 열린 파일 객체를 넘기기보다 자식 프로세스 안에서 경로를 받아 열도록 설계하는 편이 안전합니다. 함수와 클래스의 정의 모듈이 자식 프로세스에서 import 가능해야 한다는 조건도 함께 확인해야 합니다. (출처: Python pickle 문서, 한국어 multiprocessing 문서)

callable 클래스가 더 나은 경우

worker가 설정값을 여러 개 보관해야 한다면 지역 함수의 클로저로 숨기기보다, 모듈 최상위에 callable 클래스를 정의하고 인스턴스 상태를 단순하게 유지할 수 있습니다.

from multiprocessing import Process


class Worker:
    def __init__(self, prefix):
        self.prefix = prefix

    def __call__(self, value):
        print(self.prefix, value)


if __name__ == "__main__":
    worker = Worker("result:")
    process = Process(target=worker, args=(10,))
    process.start()
    process.join()

이 방식은 설정과 실행을 분리하기 쉽다는 장점이 있지만, 인스턴스에 열린 파일·락·소켓·클라이언트 세션 같은 상태를 넣으면 다시 직렬화 문제가 생길 수 있습니다. callable 클래스를 선택할 때는 “클래스인가”보다 “인스턴스 상태가 프로세스 경계를 넘을 수 있는가”를 먼저 확인하십시오.

multiprocessing 대신 스레드를 검토할 때

작업이 프로세스 분리나 CPU 병렬 처리를 요구하지 않고, 대기 시간이 큰 I/O 중심 작업이라면 ThreadPoolExecutor를 검토할 수 있습니다. 스레드 풀은 multiprocessing처럼 작업 callable과 반환값을 프로세스 간에 pickle해 전달하는 구조가 아니므로, 같은 직렬화 제약을 피하는 선택지가 될 수 있습니다. 다만 스레드가 CPU 작업을 자동으로 병렬화한다는 뜻은 아니며, 작업의 공유 상태와 오류 처리 방식까지 함께 판단해야 합니다. (출처: Python concurrent.futures 문서)

오류가 나면 이렇게 결정합니다

먼저 pickle.dumps(target)와 pickle.dumps(args)를 각각 실행해 실패 지점을 나눕니다. target이 실패하면 중첩 함수·lambda를 모듈 최상위 함수 또는 최상위 callable 클래스로 옮기고, args가 실패하면 내부 원소를 일반 데이터로 바꾸거나 자식 프로세스 안에서 자원을 열도록 재구성합니다. 둘 다 확인되었는데 실행 단계에서 문제가 남으면 main guard와 실행 환경에서 import 가능한 모듈 구조를 점검합니다. CPU 작업이 아니고 프로세스 분리가 꼭 필요하지 않다면 ThreadPoolExecutor가 더 단순한 선택인지 비교합니다.

이 순서를 따르면 “main guard를 넣었는데도 왜 같은 오류가 나는가”라는 질문에 답하면서, 함수 구조와 데이터 구조 중 실제로 바꿔야 할 부분을 좁힐 수 있습니다.

출처

Leave a Comment