파이썬 subprocess가 끝나지 않을 때는 timeout만 붙이는 것으로 충분하지 않을 수 있습니다. Popen.communicate(timeout=...)가 만료되면 프로세스가 자동으로 종료되지 않으므로, 프로세스를 종료한 뒤 communicate()를 다시 호출해 출력 파이프와 종료 상태를 정리해야 합니다. 먼저 단순한 실행 제한이면 subprocess.run(timeout=...)을 사용하고, 프로세스를 직접 제어해야 하면 이 순서를 지키는 것이 핵심입니다.
먼저 구분할 것: 느린 명령인가, 파이프가 막힌 것인가
외부 명령이 멈춘 것처럼 보여도 원인은 하나가 아닙니다. 실제 명령이 오래 실행 중일 수도 있고, stdout·stderr를 PIPE로 연결한 뒤 읽는 방식 때문에 운영체제 파이프 버퍼가 교착된 것일 수도 있습니다. 특히 stdout.read()와 stderr.read()를 차례로 직접 호출하면 한쪽 출력이 많을 때 다른 쪽을 처리하지 못해 기다릴 수 있습니다.
따라서 운영 관점에서는 다음처럼 판단합니다.
- 실행을 기다리되 제한 시간만 필요하면
subprocess.run(..., timeout=...)을 우선 검토합니다. Popen으로 실행 중인 프로세스를 직접 다루면서 출력까지 받아야 하면communicate(timeout=...)을 사용합니다.PIPE를 사용한다면wait()이나 개별read()보다communicate()를 중심으로 종료와 출력 수집을 함께 처리합니다.
단순한 경우: subprocess.run()에 timeout 지정하기
명령을 실행하고 결과를 한 번에 받는 구조라면 run()이 판단하기 쉽습니다.
import subprocess
result = subprocess.run(
["python", "worker.py"],
capture_output=True,
text=True,
timeout=10,
)
print(result.returncode)
print(result.stdout)
print(result.stderr)
run()의 timeout이 만료되면 TimeoutExpired가 발생합니다. 이 예외가 발생했다는 사실은 제한 시간 안에 명령이 끝나지 않았다는 뜻이지, 모든 자식 프로세스가 자동으로 정리되었다는 뜻은 아닙니다. 실제로 종료·출력 수집을 더 세밀하게 제어해야 한다면 Popen 흐름으로 내려가야 합니다.
직접 제어할 때의 정석: communicate() → kill() → communicate()
Popen으로 프로세스를 시작하고 제한 시간을 적용하는 기본 형태는 다음과 같습니다.
import subprocess
process = subprocess.Popen(
["python", "worker.py"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
try:
stdout, stderr = process.communicate(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
stdout, stderr = process.communicate()
print("returncode:", process.returncode)
print("stdout:", stdout)
print("stderr:", stderr)
이 코드에서 중요한 부분은 예외 처리 안의 순서입니다.
1. communicate(timeout=…)으로 기다리기
communicate()는 표준 입력을 보내고 표준 출력과 표준 에러를 수집하면서 프로세스 종료를 기다리는 메서드입니다. timeout이 만료되면 TimeoutExpired가 발생합니다.
2. TimeoutExpired에서 프로세스 종료하기
TimeoutExpired가 발생해도 자식 프로세스는 자동으로 종료되지 않습니다. 계속 실행되는 것을 원하지 않는 작업이라면 kill() 또는 상황에 맞는 종료 방식을 호출해야 합니다.
3. kill() 뒤 communicate()를 다시 호출하기
프로세스를 종료한 뒤에도 communicate()를 한 번 더 호출해야 남은 출력 파이프를 처리하고 returncode를 확인할 수 있습니다. 이때 TimeoutExpired 처리 전에 수집된 출력이 있었다면 재호출 과정에서 출력이 손실되지 않도록 처리됩니다.
wait()을 바로 쓰면 안 되는 경우
stdout=PIPE 또는 stderr=PIPE를 사용하면서 wait()만 호출하는 방식은 주의해야 합니다. 자식 프로세스가 많은 출력을 내보내 파이프 버퍼가 가득 차면, 자식은 출력 쓰기에서 멈추고 부모는 wait()에서 종료를 기다리는 교착이 생길 수 있습니다. 파이프를 사용할 때는 wait()과 별도의 read() 조합보다 communicate()를 사용해야 합니다.
운영 중 “명령이 timeout에 걸렸다”는 로그만 보고 실제 원인을 실행 시간 문제로 단정해서는 곤란합니다. 출력 파이프를 연결했는지, 두 스트림을 함께 수집하고 있는지, 그리고 제한 시간 이후에도 프로세스가 남아 있지 않은지를 같이 확인해야 합니다.
returncode로 정리 결과 확인하기
poll(), wait(), communicate()는 프로세스 종료를 확인하면 returncode를 설정합니다. returncode가 None이면 마지막 메서드 호출 시점에 프로세스가 아직 종료되지 않은 상태입니다. 따라서 timeout 처리 뒤에는 출력만 기록하지 말고 returncode도 함께 남기는 편이 안전합니다.
if process.returncode is None:
print("프로세스가 아직 종료되지 않았습니다.")
else:
print("종료 코드:", process.returncode)
kill()과 terminate()의 선택 범위
POSIX에서는 terminate()가 SIGTERM, kill()이 SIGKILL을 보냅니다. Windows에서는 kill()이 terminate()의 별칭입니다. 운영체제별 신호 차이가 있으므로 “정상 종료를 먼저 시도할지, 즉시 강제 종료할지”는 작업의 정리 가능성을 보고 정해야 합니다.
다만 여기서 다루는 절차는 단일 자식 프로세스 기준입니다. 외부 명령이 다시 여러 자식 프로세스를 만든 경우에는 프로세스 트리 전체가 같은 방식으로 종료된다고 일반화하면 안 됩니다. 그런 상황은 운영체제별 프로세스 그룹과 신호 정책을 별도로 확인해야 합니다.
적용 전에 확인할 체크포인트
저라면 다음 순서로 코드를 점검합니다.
- 단순 실행인지, 실행 중 프로세스 제어가 필요한지 확인합니다.
PIPE를 사용한다면 직접read()하고 있지 않은지 살핍니다.Popen이라면communicate(timeout=...)의TimeoutExpired처리를 둡니다.- 예외 처리에서 프로세스를 종료한 뒤
communicate()를 다시 호출합니다. - 마지막으로
returncode,stdout,stderr를 함께 기록합니다.
이렇게 하면 “명령 자체가 늦은 경우”와 “출력 파이프를 읽는 방식 때문에 멈춘 경우”를 구분할 단서가 생깁니다. shell=True를 선택할지, 외부 명령의 한글 출력을 어떻게 디코딩할지는 각각 다른 판단 문제이므로 이 글의 timeout 정리 절차와 섞지 않는 편이 좋습니다.
더 구체적으로 명령 인자와 셸 문법을 판단해야 한다면 파이썬 subprocess 명령어가 안 될 때, shell=True를 선택하는 기준을 다음 글감으로 이어서 확인할 수 있습니다.