Poetry를 이용해 Python package 통합 관리하기

이번 글은 Python 의 패키지를 관리할 수 있는 도구에 대한 글인데요, AX 에서 적용해 사용하는 것이라기보다는 제가 개인적으로 찾아서 사용해보고 경험이 좋아서 AX 개발팀을 포함한 여러 분들에게 소개해드리고자 작성하게 되었습니다.

Poetry를 이용해 Python package 통합 관리하기


안녕하세요 AX에서 서버, 백엔드 개발을 하고 있는 Kevin입니다. 이전 글들에서도 보셨겠지만 AX 에서는 Python 을 주력 언어로 사용하고 있습니다. AX 에서는 다양한 기능을 가진 서비스를 개발하다보니 여러 개의 Python 패키지들을 설치해 사용해야 하는데요, 이런 패키지 역시 지속적으로 업데이트되고 있기 때문에 우리가 사용하는 패키지의 버전을 관리할 필요가 있습니다.

이번 글은 Python 의 패키지를 관리할 수 있는 도구에 대한 글인데요, AX 에서 적용해 사용하는 것이라기보다는 제가 개인적으로 찾아서 사용해보고 경험이 좋아서 AX 개발팀을 포함한 여러 분들에게 소개해드리고자 작성하게 되었습니다.

세상에 JS 가 부러울 줄이야...

AX 에서는 backend에는 Python, frontend 에는 JavaScript 를 주 언어로 사용해 웹 서비스를 개발하고 있습니다. 그렇기 때문에 Python과 JS 패키지를 같이 관리하고 있는데요, 지금까지 python 은 requirements.txt 를, JS는 package.jsonnpm 또는 yarn 을 이용해 package 를 관리해왔습니다. 개인적으로 JS 를 별로 좋아하지 않기는 하지만, 패키지 관리를 할 때만큼은 정말 JS 가 편하다고 느낍니다. 크게 2가지 이유가 있는데, 하나는 npm이라는 잘 작동하고 활성화된 중앙 패키지 매니징 시스템이 있다는 것이고, 두번째는 (굳이 yarn 을 쓰지 않아도) package.json 안에 여러 패키지를 목적에 따라 구분할 수 있도록 한 것이나 lock file 을 만들어 의존성을 해결하고 재현성을 보장하는 것입니다. Python 에도 pypi 라는 매우 편리하고 잘 작동하는 패키지 매니징 시스템이 있습니다. 그런데 requirements.txt 는.... 좀....

requirements.txt 를 보고 있는 기분

pip 공식 문서에 보면 requirements 파일에 대한 설명이 있습니다. pip 로 패키지를 설치하는 옵션과 패키지명, 버전 등을 나열해놓은 것인데, 사실 보고 있으면 패키지를 관리 하는 파일이라기보다는 그냥 명령어 치기 귀찮고 설치할 거 많아서 파일에 나열 만 해놓은 느낌이 강하게 듭니다. pip 도 나름 똑똑해서 의존성 문제 등을 해결하는 알고리즘이 있지만 의존성 문제가 왕왕 터집니다. 그리고 의존성이 걸린 패키지의 버전까지 고정해서 다른 개발 환경 또는 서비스 환경을 완전히 동일하게 만들려면 pip freeze > requirements.txt 같은 명령어를 이용해야 하는데 이렇게 해버린 출력물을 보면 처참하기 그지없는 결과물이 보입니다. 간단하게 Python micro web framework인 flask 를 설치하며 어떤 일이 일어나는지 살펴보겠습니다.

flask 하나만 설치해도 의존성 관계로 여러 개의 패키지가 설치됩니다.

퀴즈. 어떤게 내가 설치한거고 어떤게 의존성으로 딸려온 것인지 맞추시오 (2점)

pip install flask 단 한 줄만을 친 결과를 뽑았는데 자그마치 8개가 줄줄이 사탕으로 설치되었습니다. micro framework 인 flask 의 특성 상 flask 와 같이 설치해서 사용하는 것이 한두 개가 아닌데, 이렇게 하나둘씩 패키지가 추가되면 이제 정말로 뭐가 뭔지 알 수 없는 지경이 됩니다.

기말고사. 어떤게 내가 설치한거고 어떤게 의존성으로 딸려온 것인지 맞추시오 (4점)

그럼 "의존성으로 딸려오는 패키지는 적지 말고 직접 설치한 것만 requirements 에 관리하면 되는 거 아냐? 의존성 패키지는 알아서 끌고와서 설치하는 거니까 알아서 깔리게 두면 되잖아." 라고 생각할 수 있습니다. 실제로도 그렇게 관리하는 경우도 많습니다. 하지만 우리가 직접 버전을 명시하지 않은 패키지는 설치 시에 어떤 버전이 설치될 지 알 수가 없기 때문에 패키지 버전 때문에 이슈가 생긴다면 버전을 적어 관리하지 않는 패키지는 원인을 찾아 해결하는 게 쉽지가 않습니다. 아니 뭐 그렇게까지 패키지 버전이 막 바뀌는 것도 아니니 그정도는 괜찮지 않나? 라고 하신다면...

AWS 사용자의 필수 패키지 boto3 의 의존성인 botocore. 거의 매일 버전이 올라갑니다.

이런 식으로 daily release 를 하는 패키지에서 중요한 버그픽스 같은게 있다면, 그리고 하필 그거때문에 내 앱에 문제가 생겼다면? 이제 문제 해결은 산으로 가기 딱 좋습니다. 이런 것은 찾기가 매우 어렵기 때문입니다.

그리고 또 하나,pytest 같은 패키지는 테스트를 위한 것으로, 서비스를 배포할 때는 굳이 설치할 필요가 없습니다. 이걸 requirements.txt 에 다 넣어놓으면 뭐가 실제 서비스에 필요한 것이고 아닌 것인지 구분이 가지 않기 때문에 보통은 requirements_dev.txtdev_requirements.txt 같은 파일로 따로 빼서 관리합니다. 그러면 이제 여러 개의 파일을 설치해야 하는 귀찮은 작업이 필요하게 됩니다. -r option 같은 걸 사용하면 그나마 좀 편하기는 한데 그래도 뭐 하나 설치하고 지울 때마다 이게 어디 속하는 지도 알아야 하고 그 파일 찾아서 추가하고 버전 변경하고... 이쫌 되면 둘 중 하나가 됩니다. 관리를 포기하거나 걍 다 때려박고 모르는척 하거나.

좀 치는(?) python package manager, Poetry

개발자들 사이에서 많이 하는 말이 있습니다. 니가 불편함을 느낄 정도면 이미 수천명이 같은 경험을 했을 것이고 수백명이 무언가를 만들었으며 그 중에 수십개는 널리 쓰이고 있을 거라는 말이 그것인데요, 그래서 저도 찾아봤습니다. 이 끔찍함을 해결해줄 수 있는, package.json 처럼 패키지와 의존성을 깔끔하게 관리할 수 있는 방법이 있을까? 역시 있었습니다. 이 글의 주제, Poetry 가 바로 그것입니다. 사실 Poetry 의 존재는 꽤 오래 전부터 알고 있기는 했는데, 예전에는 버그가 많아서 사용하기가 영 좋지 않아 기억 저편에 묻어두었습니다. 이제 major version 도 1로 올라왔고, 버그도 많이 잡혀 쓸만한 놈이 된 것 같아 다시 한번 사용해보고 소개해드릴 수 있을 것 같다는 생각을 했습니다.

일단 poetry 를 사용하면서 가장 편안해지는 점은 위에서 말한 것처럼 패키지를 목적에 따라 나누어 관리할 수도 있고, 의존성을 lock file 을 통해 완전하게 재현할 수 있다는 것에 있습니다. 어떤 식으로 쓸 수 있는지 예시를 통해 알아보겠습니다. 먼저 poetry 를 설치하고, 사용하고자 하는 폴더에 가서 poetry init 으로 초기화를 합니다. 마치 package.json 을 만들듯이 이름도 물어보고 버전도 물어보고 라이선스도 물어봅니다.

poetry init. 잘 모르겠으면 엔터 열심히 연타하면 됩니다.

init 이 완료되면 pyproject.toml 이라는 파일이 생성됩니다. package.json 의 작은 불편함이 바로 json 이어서 주석을 달거나 조작을 하기 까다롭다는 점인데, poetry 에서는 toml 을 사용하고 있기 때문에 이런 점에서는 JS 보다 더 사용하기가 용이하다고 할 수 있습니다.

pyproject.toml 의 기본 구조. dependencies 와 dev-dependencies 가 분리되어 있습니다.

기본적으로 파일 안에 있는 내용은 현재 프로젝트(예시의 경우 test) 의 버전과 정보를 적으면 됩니다. 아래쪽에는 이 프로젝트가 가지고 있는 의존성을 관리하는 단락이 있습니다. 위의 requirements.txt 때와 같이 flask 와 pytest 를 설치해보며 poetry 가 어떻게 패키지와 의존성을 관리하는지 살펴보겠습니다.

poetry add 명령어로 패키지를 설치할 수 있습니다.

flask 를 설치하니 역시나 여러 개의 의존성 패키지가 같이 설치되는 것을 볼 수 있습니다. 하지만 pyproject.toml 을 보면 명시적으로 설치한 flask 만 추가되어 있는 모습입니다. 나머지는 다 어디갔을까요? 바로 이 시점에서 lock file 이 등장합니다.

poetry.lock 파일에서 내가 설치한 패키지와 의존성을 확인할 수 있습니다.

poetry는 poetry.lock 파일을 만들어 패키지의 의존성을 관리합니다. 그리고 이 lock file 만 공유하면 poetry가 있는 어느 환경에서든 모든 패키지를 내가 설정한 것과 동일한 버전으로 설치할 수 있게 됩니다.

Flask 의 의존성 패키지 click. 버전과 파일명, hash 까지 꼼꼼하게 기록하고 있습니다.

새로운 환경에서는 별달리 할 것 없이 poetry install 만 해주면 알아서 lock file 을 보고 알맞은 버전을 설치해줍니다. 이제 의존성 관리를 위해 시인성 떨어지는 requirements 를 만들지 않아도 되는 동시에 의존성 패키지의 버전까지 동일하게 고정할 수 있게 되었군요. 그러면 개발용 패키지의 관리는 어떻게 따로 되는 것인지 알아보도록 하겠습니다.

-D option을 붙이면 dev-dependency 로 따로 관리가 됩니다.

-D 옵션을 추가해 개발용 패키지라는 것을 명시하면 pyproject.toml 파일 안에서 dev-dependencies 라는 섹션에 별도로 관리를 합니다. 어떤 것이 서비스에 필요하고 어떤 것이 개발용으로만 사용되는지 명확하게 알 수 있어 혼동을 줄이고 필요에 따라 원하는 것만 설치해 사용할 수도 있습니다.

"아니 어차피 poetry install 같은 거 해서 설치하면 전부 다 깔리는 거 아니냐, 왜 굳이 따로 관리해야 하느냐" 거나 "poetry 가 필요하지 않냐. 이거 하려고 여기저기에 poetry 를 깔라는 거냐" 물으신다면 대답해드리는 게 인지상정. 개발을 하는 입장에서야 패키지 관리를 편리하고 직관적으로 하기 위해 poetry 도 깔고, 개발 환경 설정을 위해 virtualenv도 만들지만, 실제 서비스를 하는 인스턴스에는 굳이 이런 설정을 다 할 필요가 없습니다. 게다가 점점 더 많은 서버가 컨테이너 기반으로 넘어가고 있는 요즘은 서비스 환경에는 아예 virtualenv 없이 시스템에 패키지를 깔고 이미지로 만들면 그만이기 때문에 이러한 복잡한 패키지 매니저나 가상환경의 필요성이 더 적다고 할 수 있습니다. 대신 여기에 필요한 것은 의존성 패키지를 포함한 모든 것이 내가 지정한 버전 그대로 설치되는 것입니다. 그러기 위해서 가장 좋은 것은 역설적이게도 아까 본 pip frezee > requirements.txt 와 같이 모든 내용이 싹 담긴 설치 목록이 됩니다. Poetry는 당연하게도(?) 저장되어 있는 패키지를 export할 수 있는 기능을 지원합니다. 이를 이용하면 내가 원하는 그대로의 내용이 담긴 requirements 파일을 얻을 수 있게 됩니다.

아까 설치한 flask 를 기준으로 의존성 패키지의 버전들이 깔끔하게 정리되어 있습니다.

그럼 개발용 패키지는? --dev 옵션을 활용하면 dev dependency 까지 모든 내용을 export 할 수 있습니다.

dev dependency 에 있는 pytest도 정상적으로 포함되어 있습니다.


패키지 매니저가 아닌 것 같은데? 오히려 좋아

Ready-made virtualenv

개인적으로 Poetry를 쓰면서 가장 만족스러웠던 부분은 requirements 따로, virtualenv 따로 관리할 필요 없이 한곳에서 개발 환경과 이 환경에 설치되는 패키지를 통합 관리해준다는 것입니다. 이게 무슨 말인고 하니, 위에서 poetry add 를 이용해 추가한 패키지들은 단순히 파일에 기록된 것이 아니라 실제로 poetry 가 따로 관리해주는 virtualenv 에 설치되어 있다는 것입니다. 터미널에서 poetry shell을 실행하면 갑자기 이상한 곳으로 가는 것을 확인할 수 있는데요, 잘 보시면 virtualenv -p python3.8 .venv 같은 것을 쓰지 않고도 개발환경이 깔끔히 준비되어 있는 것을 보실 수 있습니다.

이름이 좀 이상한 것 같지만 매우 편리하게 잘 작동합니다.

venv가 있는건 그렇다 치는데, 위치가 project root 가 아닌데 문제는 없는 걸까 하는 생각이 들 수 있겠습니다. 다행스럽게도 PATH 까지 추가해주어서 여기에 설치되어 있는 패키지를 가져다 사용하는 데는 전혀 지장이 없습니다. 프로젝트에 따라 다 알아서 따로따로 venv 를 만들어 준비해주기 때문에 설정하는걸 까먹어도 되는 것도 하나의 장점이라 할 수 있겠습니다. 그리고 또 다른 소소한 장점으로 virtualenv가 프로젝트 소스와 다른 곳에 있다보니 venv를  .gitignore 에 추가하지 않아도 되며 패키징을 하거나 배포를 할 때에도 신경쓸 필요가 없다는 점이 있습니다. 개인적으로는 이 부분이 상당히 편리하다고 느꼈습니다. 그리고 Pycharm 같은 IDE 의 경우에는 project interpreter 설정에서 poetry 를 잡아주어 바로 venv를 연결해 사용할 수 있기도 합니다.

Build as you wish

패키지를 설치하는 사람이 아니라 역으로 만드는 사람의 입장도 생각해보도록 하죠. Python 에서는 공식적으로 패키지 생성을 위한 문서를 제공하고 있고, setup.py 같은 파일을 만드는 것으로 상대적으로 쉽게 내 코드를 다른 다른 곳에 설치 가능하게 만들고, build 를 이용해 wheel 을 빌드할 수도 있습니다. 그런데 이것도 사실 파일 만들고 설정하고 하려면 은근 귀찮은 것이 사실입니다. 게다가 pypi 에 업로드까지 하려면 twine 같은 것도 있어야 하는 등 번거롭기도 하구요. 그런데 poetry 는 이런 것도 편리하게 할 수 있도록 build 기능을 제공하고 있습니다.

짜잔 빌드가 완료되었습니다.

보통은 이렇게 패키지를 만들어 공유하는 경우가 많지는 않으나, 큰 프로젝트를 진행하거나 회사 내 여러 곳에서 널리 쓰이는 기능의 경우 이렇게 따로 빼서 관리하는 게 편리한 경우가 있습니다. 이럴 때는 private pypi server를 하나 만들고 poetry 를 사용하면 간단하게 해결할 수 있습니다.

당연한 말씀이지만, 무적은 아닙니다. 그래도 이정도면 준수한데?

Poetry가 major version도 1로 올라왔고 매우 적극적으로 개발되고 있기는 하지만 당연히 여러가지 문제는 발생하고 있고, 개중에는 아예 사용할 수 없을만큼 치명적인 문제도 있습니다. 하나 예를 들어보자면, virtualenv 가 생성되지 않고 에러를 내는 경우가 가끔 있습니다. 위에서 언급했듯 poetry add 를 한 패키지는 실제로 poetry 가 관리하는 venv에 설치가 되어야 하는데, venv 를 만들 수 없으니 설치도 안 되고, pyprojcet.toml 에 추가도 안 됩니다. 이렇게 poetry의 기본 기능을 사용하지 못하게 되는 정도의 문제의 원인으로 Poetry측에서 말하는 것은 황당하게도 pip 가 아닌 apt, brew 등의 패키지 매니저로 virtualenv 를 설치해서이고, 해결법이 그거 지우고 pip로 다시 설치하는 것입니다. Poetry 버전이 올라가면서 이 문제가 더이상 발생하지 않는다고는 하는데, 아직 과도기적인 상황이라 종종 이런 일이 벌어지는 것으로 보입니다. 저도 몇번 겪으면서 상당히 불편함을 호소한 에러였습니다. 이미 설치가 되어 있는 시스템에서 Poetry 를 사용하려면 다른 무언가를 지우고 설치해야 한다는 것은 매우 큰 불편이고 다른 사용자에게 영향을 줄 수 있기 때문에 조심스러운 일입니다.

또 다른 유명한 이슈로는 serverless 를 사용하거나 CI/CD 에 poetry 를 그대로 가져가서 사용하고 싶은 경우 poetry가 스스로 관리하는 venv 의 이상한 경로가 되려 문제를 만드는 것을 들 수 있습니다. AWS lambda 를 통해 serverless 구조를 구현한다 가정하면 lambda 가 사용하는 패키지를 lambda layer 라고 하는 곳에 별도로 올려야 하는데, 이때 저 멀리 사용자의 cache directory 에 숨어있는 poetry 의 경로는 치명적인 약점으로 작용합니다. 심지어 lambda layer 를 업로드하는 과정을 CI/CD에서 자동화시키려 한다면 CI/CD 가 작동하는 컨테이너에서 poetry를 사용할 수 있도록 한 술 더 떠야 할 것이기 때문에 두배로 귀찮아집니다. 아예 미리 export 를 해서 CI/CD 는 export 한 결과를 쓰거나 별도의 venv 를 만들어 여기에 패키지를 깔도록 하거나 poetry 설정을 변경해 project root 에 venv 를 만들거나 여러가지 사용할 수 있는 방법이 있기는 하지만, 어찌되었건 특정한 환경에서는 원하는대로 부드럽게 사용할 수 없다는 것은 큰 단점이라고 할 수 있겠습니다. 이런 걸로 삽 몇번 뜨다 보면 지금 뭐하는 건가 싶기도 하고 poetry 괜히 썼나 싶기도 하다는 생각이 들 때도 있습니다.

그래도 이정도면 "패키지" 라는 것에 관련되어 있는 많은 기능을 포괄적으로 지원해주고(관리, 설치, venv, 패키징, pypi) 또 직관적이고 간결한 명령어를 통해 이러한 것들을 가능하게 하고 있기 때문에 현재 쓸만한 python package manager 중에는 가장 좋은 녀석이 아닌가 생각합니다. 이런 매니저가 나온 것도 개발하는 입장에서 환영할 일이고, 많은 사람들이 적극적으로 사용하면서 발전시키고 있기 때문에 계속 더 좋아질 것이라 생각합니다.

이번에는 개발 생산성을 위해 AX 팀원 및 여러분들과 공유하고 싶은 이야기를 해보았습니다. 새로운 기술이 항상 모든 경우에 다 잘 어울리는 것도 아니고, 환경에 따라 어떤 서비스에서는 활용하기 어려울 수도 있지만 그래도 한번 시도해보고 자신의 케이스에 도움이 되는지 알아볼 가치는 있다고 생각합니다. 이렇듯 AX 개발팀은 자유로운 탐색과 공유로 더 나은 개발 문화를 만들기 위해 노력하고 있으며, 새로운 것을 도입하는 데 적극적인 시도를 하고 있습니다. 또한 이런 시도와 공유를 즐기는 분들과 함께하길 원합니다. 이번 글을 통해 새로운 것을 도입하는 기회가 되고, 의존성 관리에 불편을 겪는 분들의 문제가 조금이라도 덜어지길 바라며 이만 줄이겠습니다. 언제나처럼 긴 글 읽어주셔서 감사합니다.

액스(AX)로 다 채널 여행 상품 동시 판매 및 운영


액스는 투어 & 액티비티 판매자의 온라인 판매를 위한 최적화된 다 채널 자동 판매 서비스 입니다.
지금 무료로 시작해보세요. 써보면서 이해하는 것이 가장 빠릅니다!