파이썬 multiprocessing RuntimeError 원인과 freeze_support() 적용 기준

파이썬 multiprocessing RuntimeError는 자식 프로세스가 메인 모듈을 다시 가져오는 동안 Process나 Pool이 다시 만들어질 때 발생할 수 있습니다. 일반 스크립트에서는 if __name__ == '__main__':으로 프로세스 생성 진입점을 보호하고, PyInstaller 같은 frozen executable에서는 그 안에서 freeze_support()도 호출해야 합니다. 두 줄은 비슷해 보이지만 담당하는 문제가 다릅니다.

먼저 구분할 것: 일반 실행인가, 패키징 실행인가

이 문제를 만났을 때 freeze_support()부터 모든 코드에 붙이는 것은 좋은 출발점이 아닙니다. 먼저 다음 두 가지를 나눠 보시면 됩니다.

  • python worker.py처럼 일반 Python 인터프리터로 실행하는가
  • PyInstaller 등으로 만든 frozen executable을 실행하는가

Python 공식 문서에 따르면 spawn 또는 forkserver 방식에서는 자식 인터프리터가 메인 모듈을 안전하게 import할 수 있어야 합니다. 메인 모듈을 import하는 과정에서 다시 프로세스를 만들면 RuntimeError가 발생할 수 있습니다. 따라서 일반 실행에서도 핵심은 freeze_support()가 아니라, 메인 모듈의 실행 코드와 프로세스 생성 코드를 분리하는 것입니다. Python multiprocessing 공식 문서는 이 상황을 safe importing of main module로 설명합니다.

일반 Python 스크립트의 기본 형태

Process나 Pool을 모듈 최상위에서 바로 만들지 말고, 실행 함수로 옮긴 뒤 main guard 안에서 호출합니다.

from multiprocessing import Pool


def square(value: int) -> int:
    return value * value


def main() -> None:
    with Pool() as pool:
        print(pool.map(square, [1, 2, 3]))


if __name__ == "__main__":
    main()

여기서 중요한 것은 main()이라는 함수의 이름이 아닙니다. 자식 프로세스가 모듈을 import할 때 실행되어서는 안 되는 작업을 if __name__ == "__main__": 아래에 두는 것이 핵심입니다. Pool()이나 Process()를 함수 안에서 만들더라도 그 함수를 모듈 최상위에서 호출하면 같은 문제가 생길 수 있으므로, 호출 경로까지 main guard 아래에 있어야 합니다.

함수와 인자도 별도로 점검해야 합니다. spawn 방식에서는 자식 프로세스가 실행할 대상과 전달할 값이 직렬화될 수 있어야 합니다. 따라서 프로세스 작업 함수는 모듈 최상위에 두고, 클로저나 실행 중에만 존재하는 객체를 인자로 넘기는 문제는 이 진입점 문제와 분리해 확인하는 편이 안전합니다. 이 글의 범위를 넘어서는 pickle 세부 오류는 별도 점검 대상입니다.

freeze_support()가 필요한 경우

freeze_support()는 일반 스크립트의 main guard를 대신하는 함수가 아닙니다. Python 공식 문서에서 이 함수는 frozen executable을 지원하기 위한 장치이며, main guard 직후에 호출하는 형태로 제시됩니다. 일반 Python 인터프리터로 실행할 때는 효과가 없습니다.

from multiprocessing import Process, freeze_support


def worker() -> None:
    print("child process")


def main() -> None:
    process = Process(target=worker)
    process.start()
    process.join()


if __name__ == "__main__":
    freeze_support()
    main()

PyInstaller 공식 문서는 frozen application에서 multiprocessing을 사용하기 전에 multiprocessing.freeze_support()를 호출해야 한다고 안내합니다. 호출하지 않으면 애플리케이션 프로세스가 반복 spawn loop에 빠질 수 있습니다. PyInstaller는 표준 라이브러리의 동작을 재정의하므로, Windows가 아닌 플랫폼에서도 frozen 실행 파일이라면 호출이 필요할 수 있습니다. 자세한 적용 조건은 PyInstaller의 Multi-processing 공식 문서를 기준으로 확인해야 합니다.

정리하면 일반 실행 파일에도 main guard는 필요하지만, freeze_support()는 frozen executable의 실행 경로를 위한 추가 처리입니다. 그래서 두 요소를 함께 작성해도 되지만, 둘의 역할을 같은 것으로 이해하면 안 됩니다.

RuntimeError가 계속될 때 확인하는 순서

운영 관점에서는 다음 순서로 범위를 좁히는 것이 효율적입니다.

  1. 오류가 발생한 실행 형태가 일반 인터프리터인지 frozen executable인지 확인합니다.
  2. 모듈 최상위에서 Process(), Pool(), Manager() 또는 이들을 호출하는 코드가 실행되는지 찾습니다.
  3. 프로세스 생성으로 이어지는 호출을 if __name__ == "__main__": 아래로 이동합니다.
  4. frozen executable이라면 main guard 직후에 freeze_support()를 추가합니다.
  5. 그 다음에야 작업 함수와 인자의 pickle 가능성, start method, IDE·노트북·웹 프레임워크의 실행 방식을 별도 문제로 확인합니다.

이 순서를 지키면 freeze_support()를 추가했는데도 계속 반복 실행되는 이유를 놓치기 어렵습니다. 반대로 일반 실행의 import 문제를 패키징 문제로 오해하면 코드 구조는 그대로 둔 채 보호 함수만 늘어날 수 있습니다.

적용 여부를 한 문장으로 판단하면

일반 Python 실행에서 RuntimeError가 난다면 먼저 프로세스 생성 코드와 호출 경로에 main guard를 적용합니다. PyInstaller로 패키징한 실행 파일에서 자식 프로세스가 반복 생성되거나 시작되지 않는다면 main guard를 유지하면서 freeze_support()를 함께 검토합니다. cx_Freeze 등 다른 패키징 도구는 도구별 공식 문서의 처리 방식을 추가로 확인해야 합니다.

다음으로 이어질 질문은 Pool을 함수 안에서 만들 때 호출부까지 어떻게 보호할지, 작업 함수와 인자가 pickle되지 않을 때 어떤 부분을 분리해 확인할지입니다. 이 글에서는 그 세부 구현보다 먼저, 오류의 원인을 일반 실행과 frozen 실행으로 나누는 판단 기준에 집중했습니다.

Leave a Comment