한동안 직접 코드를 작성하는 일을 거의 하지 못하고 관리만 하고 있다가, 최근 반 년 동안 Python과 C++를 이용해 몇 가지 작은 유틸리티를 만들어 보고 있습니다. 만들면서 새삼 느끼는 것은 Python이라는 언어는 그 언어를 도구로서 사용하여 기계 학습 등의 결과를 배포하는데는 매우 유용하지만, Python 기반의 스크립트, 즉 Python 코드 배포 작업, 특히 일반 사용자 대상으로 하는 배포는 가급적 하지 않는 것이 정신 건강에 좋다는 것입니다.

과거의 BASIC 기반의 언어가 그랬던 것처럼 인터프리터 기반의 언어가 가지는 숙명이라면 숙명이겠지만, 도구 자체의 배포가 까다롭다는 사실은 개발자가 아닌 사용자 입장에서는 전혀 매력적이지 않기 때문입니다. 기본적으로 Python은 런타임 도구가 아닌 실제 개발 도구에 해당하는 인터프리터와 표준 라이브러리, 그리고 필요한 경우 추가 라이브러리까지도 사용자가 직접 설치해야 하고, 더군다나 그 방법이 다른 애플리케이션처럼 설치 프로그램을 제공하는 것도 아니기에 더욱 더 까다롭게 느껴지는 것이 사실입니다.

Python™
Python™

PyInstaller와 Nuitka

이를 해결하기 위한 방법으로 Python 코드 배포 작업을 위해 실행 파일 형태로 패키징해 주는 몇 가지 도구들이 있긴 합니다.

그 중 가장 많이 사용되는 것이 PyInstaller로서, 최신 버전의 Python 기반 스크립트에도 무리 없이 바로 적용 가능하다는 장점이 있습니다. PyInstaller는 기본적으로 실행되는 진입 코드가 자신이 포함하고 있는 모든 스크립트 파일의 압축을 임시 디렉터리에 해제한 후, 해당 파일을 인터프리터로 직접 실행하는 구조를 가지고 있습니다. 다시 말해 스크립트 파일을 컴파일하거나 변환하는 것이 아니라 단순히 따라가며 필요한 스크립트 파일들의 목록을 작성하고 이를 하나로 묶어 주는 도구입니다. 따라서 실행 시에 스크립트 파일의 압축을 해제하는데 몇 초 정도의 시간이 필요하지만, 실행 이후에는 일반적인 Python 스크립트와 동일한 방식으로 실행됩니다.

pyinstaller
pyinstaller

하지만 소스 코드에 해당하는 스크립트 파일이 그대로 노출되기 때문에, 이를 원하지 않는 경우에는 선택하기 꺼려질 수도 있습니다. 또한 빌드 시에 오류가 발생하는지 여부를 전혀 판단하지 않기 때문에 스크립트를 작성할 때 모든 오류를 미리 철저하게 점검할 필요가 있습니다. 그리고 이러한 단점이 단점으로 여겨지지 않을 정도로 큰 단점이 하나 있는데, 단일 파일 옵션(-F 또는 --onefile)으로 빌드 시 파일의 크기가 매우 커진다는 점입니다. 물론 그럼에도 불구하고 높은 호환성과 배포 편의성을 고려한다면 가장 나은 선택일 가능성이 있습니다.

이와 비슷한 도구로 유명한 것은 cx_Freeze가 있으며, Windows 전용이지만 py2exe도 눈여겨 볼 만 합니다.

사실 Pythonista라면 Python이라는 언어의 특성 상 스크립트 파일의 노출을 크게 문제 삼을 이유가 없기 때문에, PyInstaller의 사용에 크게 거리낌이 없을 것입니다. 하지만 상용 애플리케이션을 개발하거나 다른 이유로 인해 소스 코드의 노출이 문제가 될 수 있다면, Nuitka를 고려해 볼 수 있습니다. 그리고 만약 여러분이 Nuitka를 선택하셨다면 지옥의 무한 궤도에 올라타신 것을 축하드립니다.

nuitka
nuitka

Nuitka는 PyInstaller와 달리 Python으로 작성된 스크립트를 C++11 코드로 변환하고 이를 컴파일하여 실행 파일로 만드는 일종의 컴파일러(Compiler)입니다. 따라서 실제로 C++로 개발된 애플리케이션과 마찬가지로 바이너리 코드를 바로 실행하는 구조를 가지고 있으며, Python에서 추가 설치가 필요한 라이브러리 역시 바이트 코드 형태로 사용됩니다. 쉽게 말하자면 Nuitka는 Python 스크립트를 C++ 소스 코드로 변환하는 변환기와 이를 컴파일해 주는 컴파일러를 실행해 주는 도구가 결합된 도구입니다.

당연한 이야기지만 C++ 코드 기반으로 컴파일된 실행 파일이기 때문에 스크립트를 실시간으로 번역하는 인터프리터 기반의 Python 스크립트보다 성능이 향상될 가능성이 높으며, 코드 상황에 따라 컴파일러에 의한 더 나은 최적화가 가능할 수도 있습니다. 또한 C++ 코드를 컴파일한 것이기 때문에 원본 스크립트가 노출될 일도 없습니다. 물론 C++ 코드로 디컴파일을 하려면 못할 것도 없겠지만, 그렇다고 그것이 원본 Python 스크립트인 것은 아니니까요.

하지만 스크립트를 작성하는 사람에 따라 온갖 괴랄한 테크닉이 적용되어 있을 수도 있고, 이를 C++ 소스 코드로 변환하는 변환기가 제대로 처리하지 못하는 문제가 발생할 가능성도 충분히 있습니다. 또한 변환기가 가지는 한계로 인해 최신 Python 코드를 사용하지 못할 수도 있습니다. 이 글을 작성하고 있는 11월 18일 기준 최신 버전인 2.5.1은 Python 3.13을 부분적으로 지원하는데, Python 3.13 자체가 GIL(Global Interpreter Lock) 회피와 같은 일부 기능을 시험적으로 도입한 버전이다 보니 100% 지원은 2025년 초에나 가능한 상황입니다.

Python 코드 배포 도구는 단순한 실행 파일 생성기가 아니에요

하지만 사실 PyInstaller를 비롯한 cx_Freeze, Nuitka와 같은 도구는 단순하게 Python 스크립트를 실행 파일로 만들어 주는 도구가 아닙니다.

Python으로 코드를 배포해 본 분들은 아시겠지만, 모든 스크립트가 최신 버전의 Python 번역기와 호환되는 것은 아니며, 최신 버전에서는 지원하지 않거나(deprecated) 또는 이미 제거된(removed) 기능을 활용하고 있을 가능성도 있습니다. 이런 스크립트는 실행을 위해 특정 버전의 Python 번역기를 요구하며, Python 번역기 역시 여러 개의 버전이 동시에 설치되고 실행될 수 있습니다.

더 큰 문제는 각 스크립트마다 정상적으로 설치되는 서드 파티 모듈의 버전 호환성이 다를 수 있다는 점입니다. 간혹 특정 프로그램을 설치하거나 실행하면서 서드 파티 모듈이 갱신되면, 다른 프로그램이 갑자기 실행되지 않는 문제를 겪으신 분들도 계실 것입니다. 굳이 Python을 들먹거리지 않더라도 WIndows의 DLL Hell(DLL의 버전 충돌 문제)도 동일한 문제에 해당합니다.

이를 해결하려면 각각의 스크립트를 서로 격리된(isolated) 환경에서 실행할 필요가 있는데, 이를 도와 주는 도구가 Anaconda 또는 venv와 같은 가상 환경(Virtual Environment)입니다. 개발에 대해 잘 모르는 분들이라도 Hyper-V 등과 같은 가상 환경에 새로운 운영 체제를 깔아서 테스트한 경험이 있으실 겁니다. 이와 마찬가지 개념으로 Python 번역기와 라이브러리를 하나로 묶어 격리된 환경으로 구축해 실행할 수 있습니다.

그러나 이는 어디까지나 Python 개발자에게나 가능한 이야기일 것입니다. 단순한 스크립트를 하나 실행하고 싶을 뿐인데, 이를 위해 Python 개발 환경을 설치하고, 다시 Anaconda나 venv로 가상 환경을 구축하고, 그 내부에 다시 서드 파티 모듈을 설치하고 관리하고 싶은 사용자는 아무도 없을 테니까요.

사실 PyInstaller나 cx_Freeze가 하는 일은 이러한 가상 환경 전체를 실행 파일 형태로 묶어 압축하고, 실행 시마다 이러한 가상 환경을 빌드하고 실행하는 일을 합니다. 실행 파일로 만들어 주는 것은 전면에 내세우는 기능이기는 하지만 사실 상 압축 파일을 해제하기 위해 압축 해제 프로그램을 새로 설치할 필요 없는 자체 압축 풀림 파일(SFX, Self File Extractor)로 만드는 과정에 해당하는 것일 뿐입니다.

따라서 사용자 뿐만 아니라 이를 배포하는 개발자 역시 이러한 가상 환경과 가상 환경을 배포하는 도구들에 대한 개념 이해가 되어 있어야 합니다.

또 다른 문제들

requests
requests

Nuitka로 컴파일한 프로그램에서만 볼 수 있는 특이한 문제들이 몇 가지 더 있는데, 그 중 하나가 Requests를 통해 지속적으로 API를 반복 호출하는 경우 발생하는 인증 파일 처리 문제이며, 대략 24시간에 한 번씩 certifi에서 인증 오류가 발생하기 때문에 이를 회피하기 위한 코드를 추가로 작성해 줄 필요가 있습니다. 이 외에도 종료 시그널을 던지게 되면 단일 파일 옵션으로 컴파일된 실행 파일의 경우 무조건 주 프로세스가 종료되어 버리는 문제가 있는데, 그 내부 구조 상 다음과 같이 시그널을 무시하는 코드가 있더라도 제대로 동작하지 않습니다. (참고 설명) 따라서 이에 대한 대처 코드를 추가로 작성할 필요가 있습니다.

 signal.signal(signal.SIGINT, signal.SIG_IGN)

이와 같이 Nuitka를 이용해 실행 파일 형태로 Python 코드 배포 작업을 하려는 경우, 실제 Python 스크립트의 오류도 모두 처리해 주어야 하지만, Nuitka를 사용함으로써 발생하는 부수적인 작업이 그 이상으로 꽤 높은 난이도를 자랑합니다.

그리고 Python 자체가 가지고 있는 문제 중 하나는 저수준 시스템 처리가 매우 번거롭다는 점입니다. 애초에 Python의 목적과는 동떨어진 기능이기도 하지만, 그럼에도 불구하고 그런 기능들이 필요할 때가 있습니다. 예를 들면 특정 키를 입력하면 그 값을 인식해서 반응한다라는 정말 단순한 작업이 그것입니다. 의외로 Python에서는 비차단 입력(Non-Blocking Input)이 쉽지 않은데, 다시 말해 키보드 버퍼(Keyboard Buffer)가 비어 있으면 계속 작업을 속행하고, 키 버퍼에 입력값이 들어 있으면 그 값을 꺼내 처리하는 작업이 쉽지 않습니다. 물론 키 처리 방식이 운영 체제마다 다른 것도 그 원인이지만, Python이 운영 체제별로 처리하는 코드를 100% 통합하는 것이 여전히 어렵다는 것을 보여주는 사례이기도 합니다.

맺으며

이러한 여러 가지 문제점에도 불구하고, Python은 가장 강력한 도구 중 하나이며, 최고 수준의 개발자들이 끊임없이 개선해 나가고 있는 도구이기도 합니다.

따라서 여러분이 Python 개발자, 즉 Pythonista라면 Python 코드 배포 시 갖고 있는 약점을 보완하는 방법에 대해 더 많이 고민해 보실 필요가 있습니다. 이는 단순히 해당 스크립트를 사용하는 사용자를 위하는 것이 아니라 개발자 자신을 위한 일이기도 합니다.

또한 스크립트를 사용하는 사용자 역시 단순한 프로그램으로 생각하시지 말고, 시스템의 환경이 안전하게 유지될 수 있도록 어떻게 사용하는 것이 올바른 방법인지에 대해 물어보거나 확인해 보시면 좋겠습니다. 물론 이는 매우 어려운 일이며, 이에 데해서는 조만간 새로운 글을 통해 격리 환경의 구축에 대해 도움을 드리는 글을 작성해 보도록 하겠습니다.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.