프로그래밍/Python

[python] asyncio.create_subprocess_exec()로 외부 명령 실행 시 환경 변수 설정하기

채윤아빠 2026. 2. 23. 08:58

 

Python에서 비동기 프로그래밍을 할 때, 외부 명령이나 프로그램을 실행해야 하는 경우가 종종 있습니다. 이때 "asyncio.create_subprocess_exec()"를 활용하면 비동기 방식으로 외부 프로그램을 실행할 수 있습니다. 그런데 실행하는 프로그램이 특정 환경 변수를 필요로 하는 경우, 어떻게 처리하면 좋을까요? 이번 글에서는 "env" 매개변수를 사용하여 환경 변수를 안전하게 설정하는 방법을 소개하겠습니다.

왜 환경 변수 설정이 필요한가?

외부 프로그램은 실행 환경에 따라 특정 라이브러리 경로("LD_LIBRARY_PATH")나 설정값이 필요할 수 있습니다. 예를 들어, NFC 리더와 같이 특정 공유 라이브러리 경로를 필요로 하는 바이너리를 실행하는 경우, 해당 경로를 환경 변수에 설정하지 않으면 프로그램 실행이 실패할 수 있습니다.

"asyncio.create_subprocess_exec()"는 "env" 매개변수를 통해 실행할 프로그램에 전달할 환경 변수 딕셔너리를 직접 지정할 수 있도록 지원합니다.


환경변수 전달 시, 주의할 점

주의할 점 = "os.environ.copy()" 이용

환경 변수를 설정할 때 주의해야 할 점이 있습니다. "env" 매개변수에 딕셔너리를 직접 전달하면, 기존의 시스템 환경 변수가 모두 무시되고 전달한 딕셔너리만 환경 변수로 사용됩니다. 이렇게 되면 프로그램이 다른 시스템 라이브러리에 의존하고 있을 경우 예상치 못한 오류가 발생할 수 있습니다.

따라서 "os.environ.copy()"로 현재 환경 변수를 복사한 뒤 필요한 항목만 추가하거나 수정하는 것이 가장 안전합니다.

from os import environ

current_env = environ.copy()
current_env["LD_LIBRARY_PATH"] = "/usr/lib:/usr/local/lib"


위와 같이 작성하면 기존 환경 변수를 유지하면서도 원하는 환경변수만 추가하거나 덮어쓸 수 있습니다.


관련 예제

다음은 NFC 카드 리더 외부 프로그램을 비동기로 실행하는 예제입니다.

from asyncio import create_subprocess_exec, subprocess
from os import environ
from typing import Final


class NfcReaderKey:
    """
    NFC 리더 관련 키 정의 클래스
    """
    LD_LIBRARY_PATH: Final[str] = "LD_LIBRARY_PATH"


class NfcReaderDef:
    """
    NFC 리더 관련 상수 정의 클래스
    """
    LIB_PATH: Final[str] = "/usr/lib:/usr/local/lib"
    BIN_PATH: Final[str] = "/usr/local/bin/nfc_reader"


class NfcProcessRunner:
    """
    비동기 프로세스 실행을 관리하는 클래스
    """

    async def execute_nfc_reader(self) -> None:
        """
        환경 변수를 설정하고 NFC 리더 프로그램을 비동기로 실행합니다.
        """
        # 현재 환경 변수 복사 후 수정
        current_env = environ.copy()
        current_env[NfcReaderKey.LD_LIBRARY_PATH] = NfcReaderDef.LIB_PATH

        # 프로세스 실행
        process = await create_subprocess_exec(
            NfcReaderDef.BIN_PATH,
            env=current_env,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE
        )

        stdout, stderr = await process.communicate()

        if (process.returncode == 0):
            print(f"성공: {stdout.decode().strip()}")
        else:
            print(f"오류 발생: {stderr.decode().strip()}")


async def main() -> None:
    """
    프로그램 메인 진입점
    """
    runner = NfcProcessRunner()
    await runner.execute_nfc_reader()


if (__name__ == "__main__"):
    import asyncio
    asyncio.run(main())

예제 코드 구조 설명

위 예제에서 눈여겨볼 부분들을 정리하면 다음과 같습니다.

NfcReaderKey : 환경 변수 이름처럼 외부와 상호작용하는 문자열 키를 "Final[str]" 상수로 정의합니다. 오타로 인한 버그를 방지하고, 변경 시 한 곳만 수정하면 되는 유지보수 이점이 있습니다.

NfcReaderDef : 라이브러리 경로, 바이너리 경로처럼 자주 사용하는 리터럴 값을 "Final[str]" 상수로 관리합니다. 설정값이 코드 전반에 흩어지는 것을 방지합니다.

NfcProcessRunner : 실제 프로세스 실행 로직을 OOP 방식으로 캡슐화하였습니다. "execute_nfc_reader()" 메서드에서 환경 변수 설정부터 실행, 결과 처리까지 일관된 흐름으로 관리합니다.


실시간 로그 확인이 필요하다면?

위 예제는 "process.communicate()"를 사용하여 프로세스 종료 후 출력 결과를 한꺼번에 받아오는 방식입니다. 만약 실행 중인 프로세스의 출력을 실시간으로 확인해야 한다면, "stdout.readline()"을 루프 안에서 반복 호출하는 방식으로 변경할 수 있습니다.

while True:
    line = await process.stdout.readline()
    if not line:
        break
    print(line.decode().strip())


이 방식은 로그가 긴 프로세스나, 실행 중 진행 상황을 모니터링해야 하는 경우에 유용합니다.


맺는말

"asyncio.create_subprocess_exec()"에서 환경 변수를 설정할 때는 앞서 계속 강조한 주의할 점만 기억하시면 됩니다.

  • "env" 매개변수에 딕셔너리를 직접 넘기면 기존 시스템 환경 변수가 모두 무시됨
  • "os.environ.copy()"로 기존 환경을 복사한 뒤 필요한 값만 수정하는 것이 안전한 방법임

위 주의사항을 지키면, 예상치 못한 환경 문제 없이 외부 명령 및 프로그램을 비동기/안정적으로 실행할 수 있습니다.




728x90
반응형