Huny's Dev Blog
huny.dev

Python subprocess가 출력이 많을 때 멈추는 이유: wait와 communicate로 파이프 처리하기

출력이 많은 외부 명령에서 wait가 멈추는 원인을 재현한다. Linux·Python의 6개 검증으로 양쪽 파이프 소비, 시간 초과 정리와 파일 전달의 경계를 확인한다.

Hun Jang
Hun Jang Nov 4, 2026

Python에서 외부 명령을 실행한 뒤 출력이 많을 때만 멈춘다면, stdout=PIPE·stderr=PIPE를 만든 상태로 wait()부터 호출하는지 확인한다. 유한한 출력을 모으는 배치 작업은 subprocess.run(capture_output=True, check=True, timeout=...) 또는 communicate()로 출력을 읽으며 종료를 기다린다. 이 글은 Linux에서 파이프 정체를 재현하고, 시간 초과 후 정리와 대용량 출력의 처리 방식을 비교한다.

읽지 않은 출력이 관에 쌓인 모습과 두 출력 통로를 비우는 모습을 표현한 개념 일러스트다. 실제 프로세스 구조도는 아니다.

작성·문서 확인: 2026-11-04. 실행 환경: Linux, CPython 3.12.14. Python 3.12 문서를 기준으로 설명한다. 아래는 실제 서비스나 사용자 작업의 성능 측정이 아닌, 합성 자식 프로세스를 이용한 기능 검증이다.

종료를 기다리는 부모와 출력을 쓰려는 자식

파이프는 무한한 저장소가 아니다. 가득 찬 파이프에 대한 블로킹 쓰기는 독자가 공간을 만들 때까지 진행하지 못한다. 용량을 고정된 기본값으로 가정해서는 안 된다. Linux pipe(7)

예제의 첫 사례에서는 자식이 stdout에 2 MiB를 쓰지만 부모는 읽지 않고 종료만 기다린다. 자식은 쓰기를 끝내야 종료할 수 있고 부모는 자식이 종료해야 다음 단계로 간다. 타임아웃이 없으면 이 순환 대기가 계속된다. Python의 wait 경고도 PIPE를 읽지 않을 때 생기는 이 문제를 다룬다.

stdout을 끝까지 읽고 그다음 stderr를 읽는 방식도 피한다. 예제 자식은 stdout을 쓴 뒤 stderr를 쓰며, 그동안 stdout도 열린 상태다. 부모가 stdout의 EOF를 기다릴 때 stderr가 차면 서로 진행할 수 없다. 두 스트림을 함께 처리해야 한다.

재현 코드: 대기, 수집, 실패, 시간 초과를 분리한다

아래를 subprocess_demo.py로 저장하고 Linux에서 python3 subprocess_demo.py로 실행한다. 외부 패키지는 필요 없다. fcntl과 pipesize를 사용하는 Linux 전용 재현 코드이며 Python 3.10 이상을 전제로 한다.

첫 사례는 기본 용량에 기대지 않도록 두 파이프를 4,096바이트로 설정한다. 0.2초는 정체를 관찰하기 위한 테스트 값이지 실무 권장 제한 시간이 아니다. 이후 5초는 합성 명령의 검증용 제한이다. 모든 자식은 종료하거나 명시적으로 정리하고, 마지막 임시 파일도 닫을 때 삭제한다.

실제 실행 결과는 다음과 같다.

1번에서 자식이 살아 있는 채 wait()가 시간 초과되었다. 같은 자식을 2번에서 communicate()로 이어 처리하자 stdout·stderr 각각 2,097,152바이트가 정확히 수집되고 종료 코드 0을 확인했다. 3번은 같은 작업을 run()으로 처리했다. 4번은 종료 코드 7과 오류 출력 보존을, 5번은 communicate()의 시간 초과 직후 자식이 아직 살아 있다는 사실을 검사했다. 6번은 두 스트림을 합친 임시 파일의 크기가 4,194,304바이트인지 확인했다.

유한한 배치 출력은 run, 직접 관리할 때는 communicate

실제 배치 래퍼에서는 예제 3번처럼 명령을 인자 목록으로 넘기고 종료 코드·제한 시간·출력을 함께 처리한다. 이 예제는 바이트를 비교하므로 텍스트 디코딩을 하지 않는다. 사람이 읽을 로그가 필요하면 실제 도구의 출력 인코딩을 먼저 확인한다.

Popen을 직접 관리하는 경로에서는 5번의 예외 처리가 핵심이다. communicate(timeout=...)의 시간 초과 자체가 자식을 죽이지 않으므로, 예제는 kill() 후 다시 communicate()를 호출해 종료와 남은 출력 처리를 끝낸다. 반면 run(timeout=...)은 시간 초과 시 직접 자식을 종료·회수한 뒤 예외를 낸다. Python의 communicate·run 설명

이 차이는 실행 정책에도 영향을 준다. 단순 배치는 run()으로 좁히고, 직접 만든 Popen 경로는 정상 종료뿐 아니라 예외가 발생했을 때도 자식을 누가 정리하는지 코드에서 드러내는 편이 좋다. 종료 코드가 0이 아닌 경우와 제한 시간을 넘긴 경우도 별도로 기록한다.

출력 크기에 따라 수집 방식을 바꾼다

communicate()와 capture_output=True는 출력을 메모리에 모은다. 시간 제한은 출력 바이트 제한이 아니다. 짧은 시간에도 많은 로그가 나올 수 있다. Python의 메모리 사용 주의

상황
처리 방식
확인할 경계
크기를 예측할 수 있는 유한한 결과
run의 capture_output 또는 communicate
두 스트림 합계와 메모리 여유
큰 로그를 보관하는 배치
예제 6번처럼 파일로 직접 전달
저장 용량·회전·보관 정책
출력이 필요 없는 명령
필요 없는 스트림을 DEVNULL로 전달
오류 진단 정보도 사라질 수 있음
장시간 실시간 처리
각 스트림을 계속 소비하는 별도 설계
읽기·저장 처리량과 역압력

6번에서 없앤 것은 Python 결과 객체에 전체 출력을 보관하는 버퍼다. 파일 쓰기에도 운영체제 캐시와 저장 공간이 필요하다. 파일 전달은 디스크 부족이나 느린 저장 장치까지 해결하지 않는다. 두 스트림을 합치면 출처 구분도 잃으므로 필요하면 별도 파일을 사용한다.

-u는 이 재현에서 Python 자식의 출력 버퍼링 영향을 줄이려고 넣었다. 부모가 파이프를 읽지 않는 문제를 고치는 옵션은 아니다. 파이프 용량을 늘리는 방법도 유한한 정체 시점을 늦출 뿐, 지속적인 출력 소비를 대신하지 못한다.

적용 범위와 남은 확인

이 검증은 자손 프로세스를 만들지 않는 Python 자식 하나를 대상으로 했다. 실제 도구가 다시 프로세스를 만들거나 파이프 쓰기 핸들을 물려주면, 직접 자식만 종료해도 파이프 EOF나 전체 작업 종료가 늦어질 수 있다. 프로세스 트리의 종료 정책은 별도로 검증해야 한다.

FFmpeg·TTS 엔진·클라우드 배치에서 직접 시험한 결과는 아니다. 대화형 stdin 프로토콜, 실시간 소비 구현, 무제한 출력의 메모리 상한도 이 코드의 검증 범위 밖이다. 운영 적용 시에는 같은 외부 명령을 제한된 입력으로 실행해 stdout·stderr 크기, 종료 코드와 제한 시간 후 잔여 작업을 확인한다.

English version: Python subprocess Hangs on Large Output: Fix PIPE Deadlocks with communicatePython subprocess Hangs on Large Output: Fix PIPE Deadlocks with communicate

추천 글