Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Python 개발환경 구성

Python을 못하면서도 Python이 주 언어인 회사로 이직한지 1년이 넘었지만 나는 여전히 Python을 거의 못한다. 실제로는 Python + Django까지 알아야 하지만 어쨌든 못한다. 핑계(?)를 대자면 신입이 아니다 보니 공부만 하고 있을 수는 없고 월급 받는 값은 해야 해서 내가 할 수 있는 일 위주로 하다 보니 애플리케이션은 주로 Node.js를 사용했고 AWS 인프라 관련 쪽에 좀 더 집중하고 있었다.

코드 리뷰를 해야 해서 Django 코드를 계속 보긴 했는데 직접 깊숙이 개발하는 건 아니다 보니 리뷰의 깊이는 한없이 얕았다. 이제 Django 프로젝트에 더 많이 관여해야 하므로 Python을 공부해야겠다 싶어서 개인 프로젝트를 Pyhton으로 올리기로 했다. 그동안도 계속 생각은 했지만, 개발 속도 면에서 아무래도 답답하다 보니 고민하다가 Node.js를 선택하곤 했다.

새로운 언어의 학습

작년부터 Python을 공부해야겠다고 생각한 뒤로 내가 새로운 프로그래밍 언어를 새로 배운지가 엄청 오래되었다는 걸 깨닫게 되었다. ASP도 해봤고 Java도 해봤고 JavaScript도 해봤고 새로운 기술을 좋아하는 탓에 무수한 프레임워크와 도구를 갈아타면서 새로운 것을 학습하는 데 어느 정도 자신 있다고 생각했는데 언어를 새로 배운 것은 아주 예전에 일이었다. Node.js를 하긴 하지만 Node.js는 런타임일 뿐이고 JavaScript는 신입 때부터 배웠으니 사실상 Java를 배워서 실무에 써먹기 시작한 뒤로는 새로운 언어를 배운 적이 없었다. 물론 전혀 몰랐다는 건 거짓말이겠고 2~3년 전부터 너무 JavaScript 세상에 갇혀 있는 것 같아서 걱정하고 있긴 했다.

이 부분을 깨닫고 나자 "나는 새로운 언어를 어떻게 공부했더라?" 하는 생각이 들었다. 그래서 Python 공부를 계속 미뤘는지도 모르겠다. 프로그래밍 언어에 많은 관심이 많은 편은 아니라서... 일단 Python 책을 추천받아서 읽었다. 다 읽고 나서 예전에는 아는 게 없어서 이렇게 책으로 공부했지만. 이제는 프로그래밍 언어 책을 본다고 그 언어로 프로그래밍을 할 수 있는 게 아니라는 걸 알게 되었다.

책에는 언어의 사용법이 자세히 나와 있었지만 읽는 내내 사실 좀 지루했다. 한번 훑어볼 가치는 있다고 생각하지만 읽어본다고 다 외워지는 것도 아니고 사용법은 사용하면서 익숙해지면 된다는 생각이 들었다. 예전에 배울 때는 어차피 아는 게 없었으므로 그냥 공부했던 것 같은데 지금은 현재 익숙한 환경에서 가진 지식과 어설프게 주워들어서 알고 있는 지식이 섞이면서 어떤 식으로 배워야 할지가 더 어렵게 느껴졌고 책에 나오지 않는 많은 부분이 궁금해지면서 코드를 쉽게 작성하기 어려웠다.

  • 실무에서 프로젝트를 구성할 때 환경 설정은 보통 어떻게 하지? Python은 버전도 여러 버전을 사용하는데 이 관리는 어떻게 할까?
  • Python 프로젝트에서 보이는 내가 이해 못 하는 이 파일들의 용도는 무엇일까?
  • 의존성 관리는 보통 어떤 식으로 하는가?
  • pip로 설치를 하면 이 파일들은 어디에 들어가서 관리가 되는 거지?
  • Python 개발하면서 많은 도구가 보이는데 어떤 것을 써야 하는 거고 왜 필요한 거지?
  • 폴더구조나 파일 및 코드 작성에 대한 베스트 프렉티스와 안티패턴은 어떻게 되는 거지? 이런 부분은 Python의 동작 방식에 대한 지식이 많지 않으므로 더 어렵게 느껴졌다.

오히려 예전보다 지식이 쌓였기 때문인지, 모르는 게 너무 많아서 오히려 시작하기가 어려운 느낌이었다. 예를 들어 Node.js의 경우 V8이 어떻게 동작하는지 알고 있으므로 코드를 작성할 때 이런 부분을 염두에 두고 작성하고 npm으로 패키지를 설치할 때도 어떻게 관리하는 게 좋고 어디에 설치되고 재설치할 때는 어떻게 하는지, 어떤 파일을 VCS로 관리해야 하는지에 대한 이해가 있는데 Python에서는 궁금한 건 산더미인데 가진 지식이 없다 보니 잘 시작이 안 되었었다. 일단 실무에서 사용 중인 프로젝트를 손대려고 하다 보니 더 그럴 수도 있다.

물론 자료가 없는 건 아니고 수많은 자료가 있지만 나는 전체 그림을 이해하고 상세를 이해하는 편이라서 그 수많은 자료를 이해하려면 전체를 파악할 수 있는 사전 지식이 좀 더 필요하다고 생각했다. 사실 코드를 작성하려면 못할 것도 없고 업무로 프로젝트 투입되어 2~3달 내에 웹사이트라도 만들어야 한다면 튜토리얼 보면서 대충 눈치껏 작성한다면 작성하겠지만 내가 하고 싶은 건 당장 Python으로 뭔가를 만드는 게 아니라(이미 뭔가 만들 수단은 가지고 있으므로) Python을 배우려는 것이므로 뭔가 차근차근히 해보려다가 미루기만 했던 것 같다.

결국, 이런 정보는 직접 삽질해보면서 습득하는 수밖에 없어서 사이드 프로젝트를 강제로 Python으로 진행해 보기로 했다. 그동안은 사이드 프로젝트도 익숙한 언어나 플랫폼으로 자꾸 하게 돼서 이번에는 강제로(?) Python을 선택했다. 사이드 프로젝트가 완성될지 미지수이지만 익숙한 언어로 해도 완성 안 되기는 마찬가지니까... 더불어 하나씩 해보다 보니 궁금해서 찾아보는 것도 많아질 것 같아서 공부가 많이 필요한데 그렇게 배운 내용을 정리해 보면 주변의 Python 개발자분들이 내가 잘못 이해한 걸 바로잡아주지 않을까 싶어서 글로 적게 되었다.

pyenv

Python만 그런 건 아니지만 프로그래밍 언어도 버전이 계속 올라가지만 모든 프로젝트를 같은 버전으로 맞추기는 어려우므로 보통 버전 관리자를 사용한다. Java 할 때는 프로젝트별로 버전을 다르게 쓴 기억은 별로 없어서 export JAVA_HOME=/usr/libexec/java_home -v 1.8 처럼 환경변수로 관리하거나 IDE에서 설정해서 썼던 것 같다. Node.js 같은 경우는 버전이 상당히 자주 올라가기 때문에 nvm이나 n같은 관리 도구를 사용한다. 나는 이런 도구는 한 번도 사용해 본 적이 없고 보통 Node.js 소스코드에서 빌드해서 심볼릭 링크로 바꿔가면서 사용한다. 얼마 전에 왜 그렇게 하냐는 질문을 받았는데... 음... 저런 도구가 없을 때부터 하던 습관이라고 밖에는 할 말이 없긴 했다.

어쨌든 이런 접근 방법은 사용하면서 조금씩 다듬어진 방법인데 Python은 크게 2.x와 3.x가 있으므로 Python 버전 관리자는 중요했고 회사 프로젝트도 프로젝트마다 버전이 달라져 있으므로 README에 적어두더라도 한번 환경설정을 해두면 해당 프로젝트에 들어갔을 때 자동으로 버전이 선택되고 확인할 수 있어서 크게 신경 쓰지 않기를 바랐다. Ruby에는 RVM이나 rbenv가 있는데 Python에는 pyenv가 있다. 이는 달리 고민할 도구들이 없는 거 같아서 큰 고민 없이 pyenv를 사용했다.

pyenv는 간단히 말하면 한 로컬머신 내에서 여러 Python 버전을 쉽게 바꿔가면서 사용할 수 있게 해주는 도구다. macOS에서는 Homebrew로 brew install pyenv처럼 설치할 수 있다.

설치가 끝났으면 ~/.bash_profile 파일에 export PYENV_ROOT="$HOME/.pyenvPYENV_ROOT 환경변수를 설정해 주고 마지막에 eval "$(pyenv init -)"를 추가해서 터미널에서 pyenv가 동작하도록 해야 한다. 이제 pyenv 명령어를 실행하면 버전과 명령어를 볼 수 있다.

$ pyenv
pyenv 1.1.4
Usage: pyenv <command> [<args>]

Some useful pyenv commands are:
   commands    List all available pyenv commands
   local       Set or show the local application-specific Python version
   global      Set or show the global Python version
   shell       Set or show the shell-specific Python version
   install     Install a Python version using python-build
   uninstall   Uninstall a specific Python version
   rehash      Rehash pyenv shims (run this after installing executables)
   version     Show the current Python version and its origin
   versions    List all Python versions available to pyenv
   which       Display the full path to an executable
   whence      List all Python versions that contain the given executable

See `pyenv help <command>' for information on a specific command.
For full documentation, see: https://github.com/pyenv/pyenv#readme

pyenv versions 명령어로 설치된 버전을 확인할 수 있다. * 표시가 있는 버전이 현재 사용 버전이다. 현재 활성화된 버전은 pyenv version으로도 확인할 수 있다.

$ pyenv versions
  system
  2.7.13
* 3.6.0 (set by /Users/outsider/.pyenv/version)
  3.6.2

system은 내 macOS에 기본으로 설치된 버전이고 다른 버전들은 내가 설치한 버전이다. 새로운 버전을 설치하고 싶다면 pyenv install VERSION 명령어를 사용하면 된다. 설치 가능한 버전은 pyenv install --list로 설치 가능한 버전을 확인할 수 있고 새로운 버전이 보이지 않는다면 brew update && brew upgrade pyenv로 업그레이드해야 한다.

$ pyenv install 2.7.14
Downloading Python-2.7.14.tar.xz...
-> https://www.python.org/ftp/python/2.7.14/Python-2.7.14.tar.xz
Installing Python-2.7.14...
Installed Python-2.7.14 to /Users/outsider/.pyenv/versions/2.7.14

$ pyenv versions
  system
  2.7.13
  2.7.14
* 3.6.0 (set by /Users/outsider/.pyenv/version)
  3.6.2

위는 2.7.14를 새로 설치한 것이고 아래 버전 확인에서 추가된 것을 볼 수 있다. 보통 로컬에서 기본으로 사용하는 버전이 있으므로 pyenv global 3.6.2처럼 지정하면 3.6.2를 전역으로 사용하게 된다.

$ pyenv global 3.6.2

$ pyenv versions
  system
  2.7.13
  2.7.14
  3.6.0
* 3.6.2 (set by /Users/outsider/.pyenv/version)

이제 프로젝트에서 다른 Python 버전을 사용하도록 설정해 보자. 프로젝트의 루트 폴더에서 pyenv local VERSION을 실행하면 된다.

$ pyenv local 2.7.14

$ python --version
Python 2.7.14

python local 명령어를 실행하면 현재 폴더에 .python-version 파일이 생기는데 여기에 버전이 지정되어 있다.

$ cat .python-version
2.7.14

pyenv가 이 파일을 확인해서 해당 폴더에 들어오면 지정한 버전으로 바꾸어주므로 한번 설정한 후에는 해당 프로젝트에서 지정한 Python 버전을 사용할 수 있다. 프로젝트에서 Python 버전은 같게 써야 하지만 특정 도구의 파일을 공유할 필요는 없으므로 .python-version.gitignore등에 추가해서 VCS가 관리하지 않도록 한다.

virtualenv

프로젝트를 하면 관련 패키지를 사용해야 하는데 같은 패키지라도 프로젝트마다 버전이 다르므로 프로젝트별로 이 환경을 격리해야 한다. 이 문제를 해결하는 도구가 virtualenv다. 나는 이런 류에 도구에는 익숙지 않다. Node.js는 기본적으로 현재 프로젝트 하위의 node_modules로 관리해주므로 별도로 격리하지 않아도 되고 Java에서도 클래스 패스에 의존 패키지를 두어야 하므로(아마도? Java는 이제 기억이 가물가물...) 별도로 격리 도구를 사용해 본 적이 없다. 아마 RubyGems도 Python과 비슷하지 싶은데 Ruby도 잘 몰라서 rbenv같은 도구가 해주는지 어떤지 자세히 모르겠다.

먼저 격리가 필요하다는 말은 이런 도구가 없으면 격리를 해주지 않는다는 의미이므로 원래는 어떻게 동작하는가를 알아야 했다. virtualenv로 격리하지 않으면 파이썬 버전 폴더 아래 site-packages안에 의존 패키지를 설치하게 된다. 이 폴더를 전역으로 공유해서 사용하므로 다른 프로젝트에서 같은 패키지의 다른 버전을 사용하려고 하면 문제가 발생하게 된다.

다음은 macOS의 기본 Python에서 모듈을 찾는 경로를 가진 site 모듈을 출력한 것이다. -m은 모듈을 스크립트로 바로 실행하는 옵션이다.(site라는 용어의 의미가 확 와닿지는 않는데 현재 실행환경 혹은 프로젝트 정도의 의미로 현재 이해하고 있다.)

$ python -m site
sys.path = [
    '/Users/outsider/temp/pyenv-test',
    '/Library/Python/2.7/site-packages/psycopg2-2.6-py2.7-macosx-10.10-intel.egg',
    '/Library/Python/2.7/site-packages/pyquery-1.2.13-py2.7.egg',
    '/Library/Python/2.7/site-packages/cssselect-0.9.2-py2.7.egg',
    '/Library/Python/2.7/site-packages/lxml-3.6.0-py2.7-macosx-10.11-intel.egg',
    '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python27.zip',
    '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7',
    '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-darwin',
    '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-mac',
    '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-mac/lib-scriptpackages',
    '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk',
    '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-old',
    '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload',
    '/Library/Python/2.7/site-packages',
    '/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python',
    '/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/PyObjC',
]
USER_BASE: '/Users/outsider/Library/Python/2.7' (doesn't exist)
USER_SITE: '/Users/outsider/Library/Python/2.7/lib/python/site-packages' (doesn't exist)
ENABLE_USER_SITE: True

테스트해보면 모듈을 설치하면 위 경로 중 /Library/Python/2.7/site-packages에 설치가 된다. 그리고 pyenv로 Python 버전을 선택하면 당연히 이 site 경로도 바뀌게 된다.

$ python -m site
sys.path = [
    '/Users/outsider/temp/pyenv-test',
    '/Users/outsider/.pyenv/versions/2.7.14/lib/python27.zip',
    '/Users/outsider/.pyenv/versions/2.7.14/lib/python2.7',
    '/Users/outsider/.pyenv/versions/2.7.14/lib/python2.7/plat-darwin',
    '/Users/outsider/.pyenv/versions/2.7.14/lib/python2.7/plat-mac',
    '/Users/outsider/.pyenv/versions/2.7.14/lib/python2.7/plat-mac/lib-scriptpackages',
    '/Users/outsider/.pyenv/versions/2.7.14/lib/python2.7/lib-tk',
    '/Users/outsider/.pyenv/versions/2.7.14/lib/python2.7/lib-old',
    '/Users/outsider/.pyenv/versions/2.7.14/lib/python2.7/lib-dynload',
    '/Users/outsider/.pyenv/versions/2.7.14/lib/python2.7/site-packages',
]
USER_BASE: '/Users/outsider/.local' (exists)
USER_SITE: '/Users/outsider/.local/lib/python2.7/site-packages' (doesn't exist)
ENABLE_USER_SITE: True

여기서는 모듈이 위 경로 중 /Users/outsider/.pyenv/versions/2.7.14/lib/python2.7/site-packages에 설치가 된다.site-packages와 관련해서 PEP 370가 있고 위에서 USER_SITE라는 정보가 나와서 처음에는 여기에 설치되는 줄 알았는데 이 폴더는 존재하지 않아서 정확히 USER_SITE의 의미를 아직 잘 모르겠다.

어쨌든 pyenv를 사용하더라도 위처럼 같은 python 버전 내에서는 같은 위치에 모듈을 모두 설치하므로 버전이 충돌하는 문제는 여전히 발생한다. 이를 virtualenv로 해결할 수 있는데 이름 그대로 프로젝트별로 가상환경을 만들어서 격리해준다. virtualenv 외에 virtualenvwrapperpipenv가 있고 pyenv에도 pyenv-virtualenv 플러그인이 있는데 Python에서 의존성 관리 문제를 겪어보지 않은 나로서는 각 도구의 장단점이나 차이점을 쉽게 파악하기 어려웠다. 주변에 물어보니 처음에는 그냥 virtualenv를 직접 쓰는 게 동작 방식을 이해하기 좋다고 해서 그냥 virtualenv를 쓰기로 했다.

pip install virtualenvvirtualenv를 설치한다. pyenv에서 사용하는 버전마다 설치해 주어야 하는 것으로 보인다.

이제 프로젝트 루트에 가서 virtualenv ENV_NAME으로 가상환경을 생성한다.

$ virtualenv venv
Using base prefix '/Users/outsider/.pyenv/versions/3.6.2'
New python executable in /Users/outsider/temp/pyenv-test/pyenv1/venv/bin/python3.6
Also creating executable in /Users/outsider/temp/pyenv-test/pyenv1/venv/bin/python
Installing setuptools, pip, wheel...done.

여기서는 환경 이름을 venv로 주었으므로 현재 폴더에 venv라는 폴더가 생기고 그 아래 virtualenv를 위한 파일이 아래와 같이 생성된다.

└── venv
    ├── bin
    │   ├── activate
    │   ├── activate.csh
    │   ├── activate.fish
    │   ├── activate_this.py
    │   ├── easy_install
    │   ├── easy_install-3.6
    │   ├── pip
    │   ├── pip3
    │   ├── pip3.6
    │   ├── python -> python3.6
    │   ├── python-config
    │   ├── python3 -> python3.6
    │   ├── python3.6
    │   └── wheel
    ├── include
    │   └── python3.6m -> /Users/outsider/.pyenv/versions/3.6.2/include/python3.6m
    ├── lib
    │   └── python3.6
    └── pip-selfcheck.json

당연히 이 파일을 VCS에 추가하지 말아야 하는데 일반적인 관례로 venvenv를 쓰는 것 같다.(확실하지는 않다.) 이건 생성만 한 것이므로 source ENV_NAME/bin/activate 명령어로 이 가상환경을 활성화해야 한다. 활성화되면 프롬프트 앞에 환경 이름이 표시돼서 활성화되었음을 확인할 수 있다.

$ source venv/bin/activate

(venv) $ python -m site
sys.path = [
    '/Users/outsider/temp/pyenv-test',
    '/Users/outsider/temp/pyenv-test/venv/lib/python36.zip',
    '/Users/outsider/temp/pyenv-test/venv/lib/python3.6',
    '/Users/outsider/temp/pyenv-test/venv/lib/python3.6/lib-dynload',
    '/Users/outsider/.pyenv/versions/3.6.2/lib/python3.6',
    '/Users/outsider/temp/pyenv-test/venv/lib/python3.6/site-packages',
]
USER_BASE: '/Users/outsider/.local' (exists)
USER_SITE: '/Users/outsider/.local/lib/python3.6/site-packages' (exists)
ENABLE_USER_SITE: False

site 패키지를 확인해 보면 /Users/outsider/temp/pyenv-test/venv/lib/python3.6/site-packages로 현재 프로젝트 하위에 지정되어 있을 것을 볼 수 있다.

pyvenv

Python 환경을 구성하는 도구를 검토하기가 어려워서 SNS에 질문을 올렸더니 홍민희님이 관련 도구의 역사와 용도를 블로그에 정리해서 올려주셨다. 이 글을 통해서 Python 개발환경의 역사와 각 도구의 필요성을 이해할 수 있었는데 Python 3.3부터 pyvenv라는 이름으로 virtualenv가 내장되었다는 것을 알게 되었다. 그래서 위에서는 virtualenv를 설치해서 사용했지만, Python 3.3 이상을 사용한다면 굳이 따로 설치하지 않고 내장된 pyvenv를 사용하는 게 좋겠다는 생각이 들었다.

venv문서와 PEP 405 -- Python Virtual Environments를 보면 3.3에 도입되어 원래는 pyvenv라는 명령어가 있었지만 Python 3.6부터는 deprecated되고 python -m venv를 사용하기를 권장하고 하고 있다.

$ pyvenv
WARNING: the pyenv script is deprecated in favour of `python3.6 -m venv`
usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
            [--upgrade] [--without-pip] [--prompt PROMPT]
            ENV_DIR [ENV_DIR ...]
venv: error: the following arguments are required: ENV_DIR

현재 프로젝트 폴더에서 Python 3.6 이상으로 사용한다고 할 때 python -m venv ENV_NAME으로 새로운 virtualenv를 생성할 수 있다.

$ python -m venv venv

venv가 2번 있어서 애매한데 뒤에 있는 venv가 환경의 이름이다. 아래는 venv의 폴더 구조인데 앞에서 virtualenv로 만들어진 파일과 약간 다르긴 한데 사용상에 큰 차이는 없어 보인다.

└── venv
    ├── bin
    │   ├── activate
    │   ├── activate.csh
    │   ├── activate.fish
    │   ├── easy_install
    │   ├── easy_install-3.6
    │   ├── pip
    │   ├── pip3
    │   ├── pip3.6
    │   ├── python -> /Users/outsider/.pyenv/versions/3.6.3/bin/python
    │   └── python3 -> python
    ├── include
    ├── lib
    │   └── python3.6
    └── pyvenv.cfg

현재는 Python 3.6 이상에서 python -m venv를 사용하고 그 밑의 버전에서는 virtualenv를 설치해서 사용하려고 한다.

여기서는 프로젝트 폴더에 들어가서 virtualenv를 활성화했지만 이는 폴더 기반으로 동작하는 것은 아니다. 폴더에 진입할 때 자동으로 활성화를 하려면 다른 도구를 써야 하는데 나는 direnv를 사용 중이므로 direnvPython을 연동해서 사용하고 있다. 3.6 이상에서는 python -m venv를 사용했지만, 내부는 virtualenv가 내장된 것이므로 direnv와 연동해서 사용하는데도 큰 문제가 없다. 각 도구가 역할을 잘 하고 있으므로 Python 버전 관리는 pyenv에 맡기고 direnv.envrc에서는 layout virtualenv venv처럼 지정해서 virtualenv 활성화만 할 수 있도록 했다.

홍민희 님의 글에서는 "파이썬 프로그래머이고, 여러 애플리케이션을 다양한 파이썬 버전으로 개발"하는 경우 pyenv-virtualenvwrapper를 추천하고 있지만 pyvenv를 써보니 직관적이고 간단해서 아직은 virtualenvwrapper가 필요한 이유가 잘 떠오르지 않는다. node_modules와 비슷하게 동작해서 그렇게 느껴질 수도 있지만 direnv와도 연동하고 나니 프로젝트 구성할 때만 한번 해두면 신경 쓰지 않아도 되어서 매우 편하게 느껴졌다.


이제 Python 프로젝트를 진행하기 위한 기본 환경 구성이 끝났다.

2017/10/08 15:26 2017/10/08 15:26