파이썬 subprocess FileNotFoundError, 실행 파일·PATH·작업 경로 구분법

subprocess.run()에서 FileNotFoundError, No such file or directory, 또는 Windows의 WinError 2가 발생하면 먼저 실행 파일을 찾지 못한 것인지, PATH·env가 달라진 것인지, 인자를 잘못 전달한 것인지 구분해야 합니다. 해결의 출발점은 shell=True로 바꾸는 것이 아니라 실행할 프로그램과 인자를 분리하고, 현재 프로세스가 실제로 보는 경로를 확인하는 것입니다. Python 공식 문서도 args를 시퀀스로 전달하고 실행 파일을 신뢰성 있게 지정할 때 절대경로를 고려하는 방법을 설명합니다.

예외가 발생한 위치부터 나눠 보기

subprocess.run()이 프로세스를 시작하기 전에 실행 파일을 찾지 못하면 OSError 계열 예외가 발생합니다. 반대로 프로세스는 시작했지만 명령 내부에서 실패했다면 returncode와 stderr를 살펴봅니다. 표준 오류가 비어 있을 수도 있으므로 실제 결과도 함께 살펴야 합니다. 따라서 아래 두 경우를 같은 PATH 문제로 보지 않는 것이 중요합니다.

import subprocess

result = subprocess.run(
    ["tool-name", "--input", "data.txt"],
    capture_output=True,
    text=True,
    check=False,
)

print(result.returncode)
print(result.stderr)

예제의 tool-name과 경로는 설명용이므로 실제 환경의 명령과 경로로 바꾸어야 합니다. 위 코드에서 run() 호출 자체가 예외로 끝나면 실행 파일 탐색과 인자 구성을 먼저 봅니다. 호출이 끝났는데 returncode가 0이 아니면 실제 프로그램이 실행된 뒤의 오류일 수 있으므로 stderr와 프로그램이 만든 결과도 함께 확인해야 합니다. returncode가 0이라는 사실만으로 업무 결과까지 정상이라고 단정해서도 안 됩니다.

명령 전체 문자열을 하나의 실행 파일로 넘기지 않기

subprocess.run()의 첫 인자는 실행 파일과 인자를 구분한 시퀀스로 전달하는 편이 안전합니다. 다음처럼 명령 전체를 한 문자열 원소로 만들면 Python은 공백으로 나뉜 명령이라고 자동 해석하지 않고, 그 문자열 전체를 실행 파일 이름으로 찾을 수 있습니다.

# 잘못된 형태: 실행 파일 이름에 인자까지 포함됨
subprocess.run(["tool-name --input data.txt"])

# 구분한 형태
subprocess.run(["tool-name", "--input", "data.txt"])

경로에 공백이 있는 실행 파일이라도 리스트에서는 실행 파일 경로를 하나의 원소로 두고 인자를 별도 원소로 둡니다. shell=True로 바꾸면 이 문제를 가리는 것처럼 보일 수 있지만, 셸 해석과 보안·플랫폼 차이라는 별도 판단이 생깁니다. 단순한 파일 탐색 오류의 첫 수정 방법으로 선택할 이유는 없습니다.

PATH와 실행 파일 자체를 확인하는 순서

터미널에서 실행되는 명령이 Python에서도 실행된다는 보장은 없습니다. 특히 서비스, 예약 실행, IDE에서 Python을 실행하면 로그인 셸과 다른 PATH를 물려받을 수 있습니다. 현재 Python 프로세스가 보는 PATH와 shutil.which()의 결과를 함께 기록해 보십시오.

import os
import shutil

command = "tool-name"
print("PATH:", os.environ.get("PATH"))
print("resolved:", shutil.which(command))

shutil.which()는 명령을 찾으면 실행 경로를 반환하고, 찾지 못하면 None을 반환합니다. Windows에서는 실행 파일 확장자를 찾을 때 PATHEXT도 사용하므로 확장자를 생략한 명령의 탐색 결과를 확인하는 데 도움이 됩니다. None은 해당 탐색에서 명령을 찾지 못했다는 뜻이지, 프로그램이 설치되지 않았다는 확정은 아닙니다. 설치 위치와 실행 환경의 PATH를 따로 확인해야 합니다.

운영 자동화에서는 확인된 절대경로를 명시하는 방법도 있습니다.

from pathlib import Path
import subprocess

executable = Path(r"C:\Tools\bin\tool.exe")
subprocess.run([str(executable), "--input", "data.txt"], check=True)

절대경로를 쓸 때는 해당 경로가 모든 실행 환경에서 같다고 가정하지 말고 설정값으로 관리하는 편이 낫습니다. 경로를 바꿨다면 실행 전 Path.is_file() 같은 사전 점검과 실제 예외 처리를 함께 두어 원인을 분명히 남기십시오.

cwd와 env는 무엇을 바꾸는가

cwd는 자식 프로세스의 작업 디렉터리를 정합니다. 상대 입력 파일이나 프로그램 내부의 상대 경로가 달라지는 대표적인 원인입니다. POSIX에서는 cwd가 실행 파일의 상대경로 탐색에 영향을 주고, env에 지정한 PATH가 탐색에 사용될 수 있습니다. Windows에서 shell=False이면 cwd는 실행 파일 탐색의 현재 디렉터리를 바꾸지 않고, env의 PATH도 해당 탐색에 쓰이는 PATH를 덮어쓰지 않습니다. 플랫폼 차이를 피하려면 확인된 실행 파일의 절대경로를 쓰는 편이 명확합니다.

env를 넘길 때는 필요한 환경변수를 새 사전 하나로 대체하지 않도록 주의합니다. 기존 환경을 복사한 뒤 필요한 값만 바꾸는 방식이 일반적으로 안전합니다.

import os
import subprocess

child_env = os.environ.copy()
child_env["APP_MODE"] = "batch"

subprocess.run(
    ["tool-name", "--input", "data.txt"],
    cwd="/work/job",
    env=child_env,
    check=True,
)

env에서 PATH를 직접 바꾸면 shutil.which()를 호출한 시점의 환경과 자식 프로세스의 환경이 달라질 수 있습니다. POSIX에서 자식 PATH를 따로 지정했다면 그 값을 shutil.which(command, path=child_env["PATH"])에 넘겨 탐색을 확인할 수 있습니다. 이 확인은 Windows에서 shell=False인 실제 실행 파일 탐색과 동일하다고 가정하지 마십시오. cwd는 실제 디렉터리가 존재하는지, 예약 실행 계정에 접근 권한이 있는지도 함께 확인해야 합니다.

Windows에서 명령 자체가 제거된 경우

PATH를 아무리 수정해도 운영체제에서 명령이 제거되었다면 실행 파일을 찾을 수 없습니다. Microsoft Learn은 Windows 11 버전 24H2 이상에서 WMIC가 제거되었다고 안내하며, 이 경우 단순한 PATH 누락과 다른 플랫폼 변경 사례로 판단해야 합니다.

따라서 Windows 자동화에서 wmic 같은 명령이 실패하면 다음을 구분합니다.

  • 현재 OS 버전에서 해당 도구가 제공되는지 확인합니다.
  • shutil.which("wmic")로 현재 탐색 결과를 확인하되, None만으로 제거 여부를 확정하지 않습니다.
  • 지원되는 PowerShell 명령이나 공식 대체 인터페이스가 있는지 별도로 검토합니다.
  • 대체 도구의 출력 형식과 권한이 기존 자동화와 같은지 확인한 뒤 코드를 바꿉니다.

Microsoft 문서는 해당 버전에서 WMIC를 Feature on Demand로 다시 추가할 수 없다고 설명합니다. 중요한 것은 FileNotFoundError를 만났을 때 실행 파일 부재, PATH 차이, 플랫폼 제거를 서로 다른 원인으로 기록하는 것입니다.

수정 전에 남겨야 할 진단 정보

자동화 코드를 수정하기 전에 아래 값을 같은 로그에서 비교하면 진단에 도움이 됩니다. PATH에는 민감한 경로가 포함될 수 있으므로 로그 접근 범위와 보관 방식을 살펴야 합니다.

import os
import sys
import shutil
from pathlib import Path

command = "tool-name"
print("python:", sys.executable)
print("cwd:", Path.cwd())
print("PATH:", os.environ.get("PATH"))
print("which:", shutil.which(command))

그 다음 명령을 리스트로 분리했는지, cwd가 실제 작업 디렉터리인지, env가 PATH를 덮어쓰지 않는지 확인합니다. 그래도 실행 후 실패라면 returncode, stdout, stderr, 그리고 기대한 결과 파일이나 상태를 확인합니다. 이렇게 실행 전 탐색 오류와 실행 후 업무 오류를 나누면 무작정 PATH를 늘리거나 셸에 의존하는 수정을 피할 수 있습니다.

정리

subprocess의 FileNotFoundError는 다음 순서로 좁히는 것이 실무적으로 안전합니다. 먼저 실행 파일과 인자를 리스트로 분리하고, shutil.which()와 현재 PATH로 탐색 결과를 확인합니다. 그 뒤 절대경로, cwd, env를 점검하고, Windows에서 도구가 제거된 사례인지 확인합니다. 프로세스가 시작된 다음의 실패라면 예외가 아니라 returncode·stderr·실제 결과를 기준으로 판단해야 합니다.

출처

Leave a Comment