파이썬 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가 계속될 때 확인하는 순서
운영 관점에서는 다음 순서로 범위를 좁히는 것이 효율적입니다.
- 오류가 발생한 실행 형태가 일반 인터프리터인지 frozen executable인지 확인합니다.
- 모듈 최상위에서
Process(),Pool(),Manager()또는 이들을 호출하는 코드가 실행되는지 찾습니다. - 프로세스 생성으로 이어지는 호출을
if __name__ == "__main__":아래로 이동합니다. - frozen executable이라면 main guard 직후에
freeze_support()를 추가합니다. - 그 다음에야 작업 함수와 인자의 pickle 가능성, start method, IDE·노트북·웹 프레임워크의 실행 방식을 별도 문제로 확인합니다.
이 순서를 지키면 freeze_support()를 추가했는데도 계속 반복 실행되는 이유를 놓치기 어렵습니다. 반대로 일반 실행의 import 문제를 패키징 문제로 오해하면 코드 구조는 그대로 둔 채 보호 함수만 늘어날 수 있습니다.
적용 여부를 한 문장으로 판단하면
일반 Python 실행에서 RuntimeError가 난다면 먼저 프로세스 생성 코드와 호출 경로에 main guard를 적용합니다. PyInstaller로 패키징한 실행 파일에서 자식 프로세스가 반복 생성되거나 시작되지 않는다면 main guard를 유지하면서 freeze_support()를 함께 검토합니다. cx_Freeze 등 다른 패키징 도구는 도구별 공식 문서의 처리 방식을 추가로 확인해야 합니다.
다음으로 이어질 질문은 Pool을 함수 안에서 만들 때 호출부까지 어떻게 보호할지, 작업 함수와 인자가 pickle되지 않을 때 어떤 부분을 분리해 확인할지입니다. 이 글에서는 그 세부 구현보다 먼저, 오류의 원인을 일반 실행과 frozen 실행으로 나누는 판단 기준에 집중했습니다.