subprocess: 외부 프로세스 실행하기

Python subprocess 모듈로 외부 명령어를 실행하는 방법을 설명합니다. subprocess.run(), capture_output, check, Popen, CalledProcessError 등 실무 패턴을 다룹니다.

· 5 min read · PALDYN Team

지난 글에서 shutil로 파일을 고수준으로 다루는 법을 살펴봤습니다. 이번에는 외부 명령어나 다른 프로그램을 Python 코드에서 실행하는 subprocess 모듈을 다룹니다. os.system()이라는 오래된 방법도 있지만 출력 캡처가 안 되고 보안 문제도 있어서, 현대 코드에서는 subprocess.run()이 표준입니다.

기본 사용법 — subprocess.run()

import subprocess

# 간단 실행 (출력 그대로 터미널에 표시)
subprocess.run(["ls", "-la"])

# 출력 캡처 + 텍스트 모드
result = subprocess.run(
    ["ls", "-la"],
    capture_output=True,
    text=True
)
print(result.stdout)
print(result.returncode)  # 0 = 정상

capture_output=Truestdout=subprocess.PIPE, stderr=subprocess.PIPE의 단축 표기입니다. text=True는 바이트 대신 문자열로 받습니다(인코딩은 시스템 기본값).

subprocess 프로세스 흐름

check=True — 오류 자동 예외 발생

import subprocess

try:
    result = subprocess.run(
        ["git", "clone", "https://example.com/repo.git"],
        capture_output=True,
        text=True,
        check=True   # returncode != 0 이면 CalledProcessError
    )
except subprocess.CalledProcessError as e:
    print(f"오류 코드: {e.returncode}")
    print(f"stderr: {e.stderr}")

check=True를 사용하면 외부 명령이 실패했을 때 직접 returncode를 확인하지 않아도 됩니다. CalledProcessError에는 returncode, stdout, stderr 속성이 모두 담깁니다.

입력 데이터 전달 — input 매개변수

import subprocess

# stdin에 데이터를 직접 전달
result = subprocess.run(
    ["grep", "-i", "error"],
    input="INFO: started\nERROR: failed\nINFO: ended",
    capture_output=True,
    text=True
)
print(result.stdout)  # "ERROR: failed\n"

파이프(|)로 프로세스를 연결하는 쉘 명령은 Popen을 활용하거나, 한 프로세스의 stdout을 다음 프로세스의 input으로 넘기는 방식으로 구현합니다.

타임아웃 설정

import subprocess

try:
    result = subprocess.run(
        ["curl", "https://example.com"],
        capture_output=True,
        text=True,
        timeout=10  # 10초 초과 시 TimeoutExpired
    )
except subprocess.TimeoutExpired as e:
    print(f"타임아웃! {e.timeout}초 초과")

네트워크 호출이나 장시간 실행 명령에는 항상 timeout을 지정합니다. TimeoutExpired 발생 시 자식 프로세스는 자동으로 종료됩니다.

작업 디렉토리와 환경변수

import subprocess, os

result = subprocess.run(
    ["python", "setup.py", "build"],
    cwd="/home/user/project",          # 작업 디렉토리
    env={**os.environ, "BUILD": "1"},  # 환경변수 추가
    capture_output=True,
    text=True
)

env를 지정할 때 {**os.environ, ...}처럼 기존 환경변수를 상속하지 않으면 PATH도 없어져서 명령어를 못 찾는 문제가 생깁니다.

subprocess 코드 패턴

shell=True — 주의 필요

import subprocess

# 편리하지만 보안 위험
result = subprocess.run(
    "ls -la | grep .py",
    shell=True,
    capture_output=True,
    text=True
)

# 사용자 입력을 절대 포함하면 안 됨 (명령 인젝션)
# user_input = "; rm -rf /"
# subprocess.run(f"echo {user_input}", shell=True)  # 위험!

shell=True는 쉘 파이프(|), 리다이렉션(>)이 필요할 때 편리하지만, 외부 입력을 명령에 포함하면 명령 인젝션 공격에 노출됩니다. 리스트 형식으로 인수를 분리해서 쓰는 것이 안전합니다.

Popen — 비동기 실행과 스트리밍

import subprocess

# 실시간 출력 처리 (로그 스트리밍 등)
with subprocess.Popen(
    ["tail", "-f", "app.log"],
    stdout=subprocess.PIPE,
    text=True
) as proc:
    for line in proc.stdout:
        if "ERROR" in line:
            print(f"발견: {line.rstrip()}")
            proc.terminate()
            break

subprocess.run()은 완료까지 차단(block)하지만 Popen은 비동기로 프로세스를 시작합니다. communicate() 메서드로 입출력을 한 번에 주고받거나, stdout 스트림을 직접 순회할 수 있습니다.

파이프 연결 — 두 프로세스 연결

import subprocess

# ps aux | grep python
ps = subprocess.Popen(["ps", "aux"], stdout=subprocess.PIPE)
grep = subprocess.Popen(
    ["grep", "python"],
    stdin=ps.stdout,
    stdout=subprocess.PIPE,
    text=True
)
ps.stdout.close()  # ps가 SIGPIPE를 받을 수 있게
output, _ = grep.communicate()
print(output)

정리 — 언제 무엇을

상황방법
간단 실행, 출력 필요subprocess.run(..., capture_output=True, text=True)
실패 시 즉시 예외check=True 추가
stdin 데이터 전달input="..." 추가
타임아웃 필요timeout=N 추가
실시간 출력 / 비동기subprocess.Popen() 직접 사용
쉘 파이프 필요shell=True (입력값 없을 때만)

지난 글: shutil: 고수준 파일 복사·이동·압축

다음 글: glob과 fnmatch: 파일 패턴 매칭


읽어주셔서 감사합니다. 😊