파이썬 예약 실행이 터미널에서는 되는데 실패할 때 확인 순서

터미널에서는 되지만 cron이나 launchd 예약 실행에서 실패한다면, 먼저 Python 인터프리터와 작업 디렉터리, 환경변수, 표준 오류 로그를 터미널 실행과 나란히 비교해야 합니다. 코드가 갑자기 달라진 것이 아니라 예약 실행이 다른 실행 문맥에서 시작된 경우가 많기 때문입니다. 아래 순서대로 확인하면 FileNotFoundError, ModuleNotFoundError, command not found를 한꺼번에 추측하지 않고 범위를 좁힐 수 있습니다.

1. 실제로 실행된 Python을 먼저 확인합니다

터미널에서 python을 입력했을 때와 예약 작업이 실행한 Python이 같다고 가정하면 진단이 꼬입니다. 가상환경을 사용한다면 예약 명령에 가상환경의 Python 절대 경로를 직접 적는 편이 판단하기 쉽습니다. sys.executable은 현재 프로세스를 실행한 인터프리터를 보여 주므로, 진단용 출력에 포함할 수 있습니다.

from pathlib import Path
import os
import sys

print("executable:", sys.executable)
print("prefix:", sys.prefix)
print("cwd:", Path.cwd())
print("script:", Path(__file__).resolve())
print("PYTHONPATH:", os.environ.get("PYTHONPATH"))
print("sys.path:")
for item in sys.path:
    print("  ", item)

이 코드는 예약 실행에서 실제로 사용된 인터프리터와 현재 작업 디렉터리를 출력합니다. Python 문서에 따르면 sys.path는 모듈을 찾는 경로 목록이며, PYTHONPATH가 그 초기화에 영향을 줄 수 있습니다. 따라서 모듈 오류가 나면 설치 여부만 보지 말고 sys.executable, sys.prefix, sys.path를 함께 비교해야 합니다.

2. 스크립트 경로와 현재 작업 디렉터리를 분리합니다

Path('data/input.json')처럼 상대 경로를 사용하면 기준은 스크립트 파일의 위치가 아니라 현재 작업 디렉터리가 될 수 있습니다. 터미널에서는 프로젝트 폴더에서 실행했지만 예약 작업은 다른 위치에서 시작했다면 같은 코드가 다른 파일을 찾게 됩니다.

운영 작업에서는 다음처럼 기준을 명시하는 방식이 안전합니다.

from pathlib import Path

PROJECT_DIR = Path(__file__).resolve().parent
INPUT_FILE = PROJECT_DIR / "data" / "input.json"
print("input:", INPUT_FILE)
print("exists:", INPUT_FILE.exists())

프로젝트 구조상 기준 폴더가 파일의 부모가 아니라면 그 의도에 맞는 상위 경로를 명시합니다. 핵심은 예약 실행이 우연히 어느 폴더에서 시작했는지에 기대지 않는 것입니다. launchd를 사용하는 macOS 작업이라면 작업 정의의 WorkingDirectory도 확인 대상입니다.

3. 환경변수는 값보다 실행 문맥을 비교합니다

예약 작업에서 환경변수가 비어 있거나 다른 값이면 인증 정보, 경로, 모드 선택이 달라질 수 있습니다. 다만 이 단계에서 비밀값을 로그로 남기면 안 됩니다. 변수의 존재 여부와 안전하게 마스킹한 일부 정보만 진단하십시오.

import os

for name in ("PATH", "PYTHONPATH", "APP_ENV"):
    value = os.environ.get(name)
    print(name, "set=" + str(value is not None))

특히 PATH에 의존한 python 또는 외부 명령 호출은 예약 실행에서 달라질 수 있습니다. 실행할 Python은 절대 경로로 지정하고, 필요한 환경변수는 예약 작업 정의나 안전한 별도 설정에서 명시하는 편이 낫습니다. dotenv를 바로 추가하기 전에 먼저 예약 실행에서 실제로 어떤 환경이 들어왔는지 확인하면 원인을 섞지 않을 수 있습니다.

4. 표준 출력과 표준 오류를 파일로 연결합니다

화면에서 실행할 때는 보이던 예외가 예약 실행에서는 사라진 것처럼 보일 수 있습니다. 실패 여부만 확인하지 말고 stdout과 stderr가 어디에 기록되는지 정해야 합니다. 예외의 마지막 줄만 보지 말고, 앞부분의 인터프리터·작업 디렉터리 진단 출력과 함께 시간 순서로 확인하십시오.

macOS launchd 작업은 ProgramArguments에 프로그램과 인자를 토큰별로 지정하고, WorkingDirectory로 작업 폴더를 정할 수 있습니다. StandardOutPath와 StandardErrorPath를 연결하면 실행 결과와 오류를 파일에서 확인할 수 있습니다. Apple 문서에는 실행 사용자를 지정하는 UserName·UID·GroupName·GID 설정도 설명되어 있으므로, 사용자 차이가 의심될 때 함께 확인합니다.

cron은 구현체와 운영체제에 따라 세부 환경이 달라질 수 있으므로, 예약 명령 한 줄만 보고 공통 동작이라고 단정하지 않는 편이 안전합니다. 어느 예약 도구를 쓰든 실행 파일 경로, 작업 디렉터리, 환경변수, 로그 경로를 같은 진단 출력으로 비교하는 것이 핵심입니다.

오류별로 다음 확인 위치를 고르는 기준

  • command not found: 예약 작업이 참조한 명령의 절대 경로와 PATH를 확인합니다.
  • ModuleNotFoundError: sys.executable, sys.prefix, sys.path를 터미널 결과와 비교합니다.
  • FileNotFoundError: Path.cwd()와 실제 파일의 절대 경로, exists() 결과를 확인합니다.
  • 오류가 보이지 않음: 예약 작업의 stdout·stderr 연결과 실행 사용자를 확인합니다.

이 네 가지는 서로 배타적인 원인이 아닙니다. 예를 들어 다른 Python이 실행되면서 모듈 경로가 달라지고, 동시에 작업 디렉터리도 달라질 수 있습니다. 그래서 오류 문구 하나를 고치는 것보다 처음에는 같은 진단 정보를 두 실행 방식에서 수집하는 편이 재발 방지에 유리합니다.

마무리: 먼저 고정하고, 그다음 자동화합니다

저라면 예약 설정을 고치기 전에 sys.executable, sys.prefix, Path.cwd(), 스크립트 절대 경로, 필요한 환경변수의 존재 여부를 로그로 남깁니다. 그 결과를 보고 가상환경 Python과 스크립트의 절대 경로를 지정할지, WorkingDirectory를 설정할지, 환경변수 정의를 보완할지, 표준 오류 로그를 연결할지 결정합니다.

이 순서로도 원인이 남는다면 다음 질문은 예약 도구의 종료 코드와 중복 실행 관리입니다. 환경 차이를 먼저 고정해 두면 그때부터는 스케줄 자체의 문제인지, Python 코드의 문제인지 분리해서 살펴볼 수 있습니다.

Leave a Comment