Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.

Flask 애플리케이션 개발 환경 구성

Python 개발 환경을 구성했으니 이제 프로젝트를 시작해야 한다. 나는 주로 웹 쪽을 하므로 웹 프레임워크를 선택해야 했고 대표적으로 djangoFlask가 있는데 개인적으로 Flask 쪽에 더 맘에 가긴 했지만, 더 좋은 프레임워크도 있을 수 있으므로 Facebook에 질문을 올렸다. 많은 분이 의견을 주셨지만 홍민희님의 대답이 맘에 들어서 여기에 남긴다.

파이썬 웹 프로그래밍을 처음 시작하신다는 배경 하에, Flask의 큰 장점은 파일 하나만 보는 것으로 시작할 수 있다는 점입니다. 예를 들어 Django 등의 풀스택 웹 프레임워크는 일반적으로 킥스타터의 도움을 받아 초기 파일들을 생성해주는 방식으로 시작하게 되는데, 생성된 파일이 적지 않으므로 처음 시작할 때 부담이 될 수 있습니다. 물론 이 부분은 개인차가 있을 수 있습니다. 만들어진 파일 하나 하나가 어떤 역할을 하는지 알지 않으면 신경이 쓰이는 저 같은 경우에는 Flask처럼 파일 하나로 시작하고, 그 파일조차 제가 직접 에디터에 입력하는 것으로 출발할 수 있는 쪽이 오롯이 지금 배우기 시작하는 분야에 집중하기 좋았습니다. 반면 딱히 생성되는 파일들이 어떤 역할을 하는지 이해하지 않더라도, 지금은 쓰지 않더라도 나중에 쓸 날이 오겠지 생각하고 넘어갈 수 있다면 Flask의 이런 점은 별 장점은 아닐 수 있습니다.

회사에서는 주로 django를 쓰고 있어서 django를 배워야 하긴 하지만 일단 그 전에 파이썬에 대해서 이해할 게 더 많다고 느껴졌다. 여태 개발해오면서도 Rails나 django 식의 올인원(?) 프레임워크를 안 써봐서 그런지 왠지 모를 거부감이 있었다. 사용하다 보면 결국 내부를 자세히 이해해야 잘 쓸 수 있겠지만 위에 홍민희 님이 얘기하신 것처럼 추상화 레벨이 너무 높아서 감춰진 영역이 너무 많게 느껴졌다. 빨리 웹사이트를 만들고 어드민 기능까지 바로 되어서 편할 수도 있지만, 나한테는 알 수 없이 동작하는 블랙박스처럼 느껴졌다. 그러고 보면 그동안 내가 관심 가졌던 웹 프레임워크 사이트에 들어가면 주로 "Sinatra inspired"라고 쓰여 있긴 했었다. 정작 Sinatra는 한 번도 안 써봤지만...

그리고 너무 갖추어진 환경에서 개발하는 걸 별로 안 좋아하는 게 해당 기능이 어디서 제공하는 건지 알 수 없다는 부분도 있다. 예를 들면 A 기능을 django에서 제공한다고 했을 때 이 A 기능이 원래 Python에서도 보통 사용하는 기능인데 django가 감싸거나 확장해서 제공하는 것인지 아니면 다른 Python 프로젝트에서는 없는 기능인데 django만의 기능인지를 처음 사용하는 처지에서는 구분하기 어려워진다. 각 파일의 용도나 관례를 좀 파악한 다음에 추상화된 계층을 보는 게 더 이해하기 쉽다고 생각했다.

Flask 프로젝트 구성

그래서 Flask를 사용하기로 하고 구성하기로 했다. 일단 Flask 홈페이지에 나와 있는 대로 pip install flaskflask를 설치했다.

$ pip install flask
Collecting flask
  Downloading Flask-0.12.2-py2.py3-none-any.whl (83kB)
    100% |████████████████████████████████| 92kB 1.1MB/s
Collecting Jinja2>=2.4 (from flask)
  Downloading Jinja2-2.9.6-py2.py3-none-any.whl (340kB)
    100% |████████████████████████████████| 348kB 2.7MB/s
Collecting click>=2.0 (from flask)
  Downloading click-6.7-py2.py3-none-any.whl (71kB)
    100% |████████████████████████████████| 71kB 3.1MB/s
Collecting itsdangerous>=0.21 (from flask)
  Downloading itsdangerous-0.24.tar.gz (46kB)
    100% |████████████████████████████████| 51kB 5.9MB/s
Collecting Werkzeug>=0.7 (from flask)
  Downloading Werkzeug-0.12.2-py2.py3-none-any.whl (312kB)
    100% |████████████████████████████████| 317kB 2.8MB/s
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask)
  Downloading MarkupSafe-1.0.tar.gz
Building wheels for collected packages: itsdangerous, MarkupSafe
  Running setup.py bdist_wheel for itsdangerous ... done
  Stored in directory: /Users/outsider/Library/Caches/pip/wheels/fc/a8/66/24d655233c757e178d45dea2de22a04c6d92766abfb741129a
  Running setup.py bdist_wheel for MarkupSafe ... done
  Stored in directory: /Users/outsider/Library/Caches/pip/wheels/88/a7/30/e39a54a87bcbe25308fa3ca64e8ddc75d9b3e5afa21ee32d57
Successfully built itsdangerous MarkupSafe
Installing collected packages: MarkupSafe, Jinja2, click, itsdangerous, Werkzeug, flask
Successfully installed Jinja2-2.9.6 MarkupSafe-1.0 Werkzeug-0.12.2 click-6.7 flask-0.12.2 itsdangerous-0.24

Flask를 설치하자 MarkupSafe, Jinja2, click, itsdangerous, Werkzeug, flask가 모두 설치되었다. 어디서 의존성이 같이 내려왔는지 궁금하지만 일단은 홈페이지에 나온 대로 진행을 해보자. 다음의 내용으로 hello.py 파일을 만들었다.

# hello.py
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

이제 FLASK_APP=hello.py flask run으로 실행을 하자 Hello World 웹사이트가 잘 떴다.

$ FLASK_APP=hello.py flask run
 * Serving Flask app "hello"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

여기까지만 했는데도 코드와 관련 없이 궁금한 게 많이 생겼다.

  • pip install flask만 하니 관련 의존성이 다 설치되었는데 이 의존성은 어떻게 관리되는가?
  • FLASK_APP 환경변수의 의미는 무엇일까?
  • flask 애플리케이션을 실행했더니 현재 위치에 __pycache__ 폴더가 생겼는데 이 폴더의 용도는 무엇일까?
  • 이 프로젝트가 flask를 사용하므로 소스코드를 다운받은 후 flask를 설치하도록 의존성 관리를 해야 하는데 이는 어떻게 하는가?

Python에 대해서 아는 게 참 없구나 싶은 기분이었다. 다른 사람도 Node.js 처음 하면 이렇게 궁금한 게 많아지는가 싶기도 하면서...

flask의 의존성 관리

flask 저장소로 가봤다. 최근에 본 파이썬 3에 뛰어들기에서 setup.py로 패키징하는 내용이 있어서 setup.py를 열어보니 다음과 같은 부분이 있었다.

setup(
    name='Flask',
    version=version,
    url='https://github.com/pallets/flask/',
    license='BSD',
    author='Armin Ronacher',
    author_email='armin.ronacher@active-4.com',
    description='A microframework based on Werkzeug, Jinja2 '
                'and good intentions',
    long_description=__doc__,
    packages=['flask', 'flask.json'],
    include_package_data=True,
    zip_safe=False,
    platforms='any',
    install_requires=[
        'Werkzeug>=0.9',
        'Jinja2>=2.4',
        'itsdangerous>=0.21',
        'click>=4.0',
    ],
    extras_require={
        'dotenv': ['python-dotenv'],
        'dev': [
            'blinker',
            'python-dotenv',
            'greenlet',
            'pytest>=3',
            'coverage',
            'tox',
            'sphinx',
            'sphinxcontrib-log-cabinet'
        ],
    }
)

이 파일이 Node.js의 package.json과 같은 역할을 하는 것으로 보였다. 프로젝트에 대한 설명이 있고 install_requires에 있는 의존성이 앞에서 설치된 패키지와 같은 것으로 보아 이 파일을 참고해서 설치한 것으로 보인다. extras_requireflask를 가져다 쓸 때는 필요 없지만 개발할 때만 필요한 개발 의존성으로 보인다.

virtualenv를 생성하고 설치했으므로 설치된 위치를 확인해 보니 현재 프로젝트의 ./venv/lib/python3.6/site-packages/ 아래에 다음과 같이 설치되어 있다. venvvirtualenv의 가상환경 이름이다.

├── Flask-0.12.2.dist-info
├── Jinja2-2.9.6.dist-info
├── MarkupSafe-1.0.dist-info
├── Werkzeug-0.12.2.dist-info
├── __pycache__
├── click
├── click-6.7.dist-info
├── easy_install.py
├── flask
├── itsdangerous-0.24.dist-info
├── itsdangerous.py
├── jinja2
├── markupsafe
├── pip
├── pip-9.0.1.dist-info
├── pkg_resources
├── setuptools
├── setuptools-36.5.0.dist-info
├── werkzeug
├── wheel
└── wheel-0.30.0.dist-info

setup.py가 Python 코드라서 용도를 잘 모르겠는 코드가 있는 것은 둘째치고라도 [파이썬 3에 뛰어들기](https://blog.outsider.ne.kr/1296)에서는 상단에서 from distutils.core import setup로 쓰고 있었는데 flask의 코드를 보면 from setuptools import setup로 되어 있다. 눈치껏 setup은 같은 역할을 하는 것 같은데 distutilssetuptools이 뭐가 다르고 왜 다르게 사용하는지 모르겠다. 궁금한 게 계속 나와서 진도를 못 나가...

Stackoverflow에서 잘 정리된 답변을 발견했다.

  • Distutils: 아직 Python 패키징의 표준 도구로 Python 2부터 3까지 표준 라이브러리로 포함되어 있다.
  • Setuptools: Distutils의 부족한 부분을 채우려고 개발된 도구로 표준 라이브러리에 포함되어 있지 않다. 여기서 easy_install이 도입되었다.

패키징해서 배포를 안 해봐서 아직 감이 다 오진 않지만 PyPI에 패키지를 올릴 때 사용하는 도구로 대충 이해했다. 둘 다 Python 패키지를 설치하는 도구인 pip와 잘 동작한다고 한다. 저 글의 작성자는 Setuptoolsvirtualenvpip에서 아주 잘 동작하므로 권장하고 있었다. 나중에 쓸 때 Setuptools를 자세히 찾아봐서 쓰면 되겠다 싶다.

관련해서 찾다 보니 몇 가지 더 궁금한 단어들이 생겼다.

  • wheel: Python의 패키지 형식으로 .whl 확장자를 가지고 ZIP 형식이다. virtualenv 생성하면 wheel 커맨드라인 명령어도 생기던데 아직 용도는 잘 모르겠다.
  • easy_install** : Setuptools에 포함된 도구라고 한다. 이게 궁금했던 건 virtualenv를 생성하면 easy_install 커맨드라인 명령어가 생겨서였는데 이전에는 무심코 넘어갔는데 virtualenv를 생성할 때 Installing setuptools, pip, wheel...done.라는 문구가 나오는 걸 깨닫게 되었다. virtualenv에서 이 설치 과정까지 포함된 것으로 보인다. pip vs easy_install를 보면 easy_install은 2004년에 setuptools의 일부로 릴리스 되었고 pip는 2008년에 easy_install을 대체하려고 나왔다. 그리고 Python 프로젝트 볼 때 나한테는 어색하게 느껴졌던 requirements.txt가 pip에서 도입되었다고 한다.
  • Eggs: pip vs easy_install을 보다 보니 Eggs란 용어가 나왔다. setuptools에서 도입된 빌드된 배포형식인데 Eggs are to Pythons as Jars are to Java..라는 설명을 보니 감이 왔다. pip vs easy_install 문서의 비교표를 보면 pipwheel을 쓰고 easy_installEggs를 쓰는 것 같다. 여기서 built distribution이라는 용어가 나오는데 설명을 보면 설치할 시스템에 복사만 하면 되는 배포 형식이라고 하는데 이 설명을 보면 wheel은 설치 전에 빌드 과정이 필요하므로 컴파일되지 않은 Python 파일이 담겨 있고 반대로 Eggs는 컴파일된 결과만 포함하고 있는 것이 둘의 가장 큰 차이로 생각된다.

예전에 Python의 패키지 관리 도구의 험난한(?) 역사에 대한 글을 본 기억이 있는데 그 긴 역사를 거치면서 남은 잔재가 아닌가 싶다.

FLASK_APP

Flask 홈페이지에서 FLASK_APP=hello.py flask run로 실행하도록 안내하고 있는데 Quickstart를 보면 app = Flask(__name__)에서 애플리케이션의 모듈이나 패키지 이름으로 Flask 인스턴스를 생성하고 있다. 여기서 __name__은 현재 모듈의 이름을 담고 있는 Python의 내장 변수이다.

cli 사용법을 보면 FLASK_APP 환경변수를 flask 커맨드라인 명령어가 애플리케이션을 찾을 수 있게 하는 것으로 보인다. 이 이름을 통해 템플릿이나 정적 파일을 어디서 찾아야 하는지 찾는다고 한다. 자세한 건 Flask 앱을 개발해봐야 더 이해할 수 있을 것 같다.

__pycache__

인터넷에서 찾아보니 Python 3.2부터 도입되었는데 컴파일된 바이트 코드가 들어간다고 한다. Python 2.x의 경험이 별로 없어서 몰랐지만 Python 2.x에서는 hello.py가 있으면 그 옆에 hello.pyc 바이트 코드 파일이 생기지만 3.2부터는 이 파일을 __pycache__ 아래에 모은다고 한다.

이 관련 내용이 PEP 3147 -- PYC Repository Directories인데 이 문서를 읽어보면 성능향상을 위해서 Python 인터프리터가 컴파일한 바이트 코드를 파일시스템에 저장(.pyc)하고 이후에는 컴파일 단계를 건너뛰어서 더 빨리 모듈을 로딩할 수 있게 한다. CPython만 사용해 봤지만 여러 버전의 Python 인터프리터를 사용하는 경우 인터프리터 간에 .pyc를 공유할 수 없으므로 __pycache__를 만들어서 여러 인터프리터의 바이트 코드 파일을 같이 만들어 둘 수 있도록 하기 위함으로 보인다.

그래서 바이트 코드 파일에 매직 태그가 붙는데 CPython v3.6은 .cpython-36.pyc가 된다. 실제로 위 hello.py 파일을 실행한 결과는 hello.cpython-36.pyc__pycache__안에 생긴 것을 확인할 수 있다. 당연히 이 폴더는 VCS에서 관리할 필요가 없으므로 .gitignore등에서 제외해야 한다.

프로젝트의 의존성 관리

프로젝트를 개발하면서 필요한 의존성(현재까지는 flask)을 pip로 설치해서 사용하면 되는데 다른 사람이 보면 어떤 의존성이 필요한지 알 수 없으므로 프로젝트에 이 정보를 넣어야 한다. 앞에서 flask를 설치할 때 자동으로 jinja2 등이 설치된 것처럼 패키지로 배포했을 때도 이 정보가 필요하고 GitHub 등에 소스를 공개해두었을 때도 다른 사람이 다운받아서 실행하려면 프로젝트 내에 의존성이 명시되어있어야 하고 어떤 약속된 명령어를 실행했을 때 자동으로 관련 의존성을 모두 설치할 수 있어야 한다.

아직 Python에서 이 의존성을 어떻게 관리하는 알지 못하지만, 처음부터 이런 관리에 대해 구성을 해놓고 사용하고 싶었다. Flask 저장소에서 예제를 제공하고 있어서 일단 여기를 참고했다. 코드를 열어보니 맨 위에 # -*- coding: utf-8 -*-가 있어서 Python 2.x에 맞춰진 것 같아서 좀 불안했지만, 의존성 관리가 달라졌을 것 같진 않아서 그냥 참고했다.

마이크로 블로그 예제인 flaskr을 보니 소스코드는 flaskr/디렉터리에 넣고 테스트코드는 tests/ 폴더 아래 둔다는 것을 알았다. flaskr 프로젝트 아래 src같은 폴더가 아니라 flaskr이라는 프로젝트 이름의 폴더를 한 번 더 만드는 게 이상했지만(flaskr/flaskr처럼 되니까...) 일단 그 전에 의존성을 먼저 해결해야 했기에 넘어갔다.

먼저 눈에 띈 파일은 setup.pysetup.cfg였다.

setup.py

앞에서 Flask의 setup.py를 본대로 이 파일에 필요한 의존성이 명시되어 있다. 예제로 참고하는 flaskrsetup.py는 다음과 같이 되어 있다.

# -*- coding: utf-8 -*-
"""
    Flaskr Tests
    ~~~~~~~~~~~~
    Tests the Flaskr application.
    :copyright: (c) 2015 by Armin Ronacher.
    :license: BSD, see LICENSE for more details.
"""

from setuptools import setup, find_packages

setup(
    name='flaskr',
    packages=find_packages(),
    include_package_data=True,
    install_requires=[
        'flask',
    ],
    setup_requires=[
        'pytest-runner',
    ],
    tests_require=[
        'pytest',
    ],
)

앞에서도 setup.py로 의존성을 관리하는 부분을 살펴봤는데 좀 더 이해할 필요가 있어 보였다. 이 파일이 Distutils부터 Setuptools까지 같이 쓰고 있는 것 같은데 setup.py에 대한 기본적인 내용은 https://docs.python.org/3/distutils/setupscript.html에 나와 있다.

  • name은 패키지 이름이다.
  • packages는 여기서 지적한 모듈을 Distutils가 찾아서 처리하도록 한다고 한다. 그래서 packages = ['foo']로 지정하면 foo/__init__.py를 찾는다.

아래 키워드들은 Setuptools에서 추가하거나 확장한 키워드였다.

  • find_packages()packages를 수동으로 지정하는 게 큰 프로젝트에서는 어려우므로 이 함수로 프로젝트 폴더에서 찾아서 목록을 만들어 준다고 한다.
  • include_package_dataTrue로 지정하면 MANIFEST.in에서 지정한 패키지 디렉터리에서 찾은 데이터 파일을 자동으로 포함한다.
  • install_requires는 설치할 때 필요한 다른 패키지의 목록이다.
  • setup_requires는 setup script를 실행할 때 필요한 패키지 목록이다.
  • tests_require는 테스트에 필요한 패키지 목록이다. test 명령어를 실행할 때 setuptools가 이 패키지를 가져온다.

그리고 파이썬의 개발 "환경"(env) 도구들을 보면 다음과 같이 나와 있다.

pip 같은 게 없던 때에는 라이브러리 타르볼을 받아서 푼 다음 python setup.py install 명령을 실행하는 것이 일반적인 라이브러리 설치법이었습니다. 지금도 pip*.whl 파일이 아닌 *.tar.gz/*.zip 파일인 패키지를 설치할 때 내부적으로 python setup.py install 스크립트를 실행합니다.

일단 여기에서 어감으로 볼 때 pip가 있는 지금은 python setup.py install 명령어를 사용할 필요가 없어 보였다. Stackoverflow를 보니 다음과 같이 나와 있었다.

NO. NEVER EVER do sudo python setup.py install whatever. Write a ~/.pydistutils.cfg that puts your pip installation into ~/.local or something. Especially files named ez_setup.py tend to suck down newer versions of things like setuptools and easy_install, which can potentially break other things on your operating system.

정확히 이유는 모르겠지만 버전을 깨뜨릴 수 있으니 python setup.py install을 쓰지 말라고 하므로 일단 사용 안 하면 되겠다 싶었다. 이 내용은 정확히는 python setup.py install을 쓰면 안 된다기 보다는 sudo로 설치하지 말라는 얘기라고 한다. 글을 볼 때 pipsetup.py installl의 차이를 검색하면서 들어갔고 라이브러리나 애플리케이션 코드는 sudo로 설치하지 않는 것은 보안상 일반적인 관례이므로 그 부분을 얘기한다는 것은 생각하지 못했다.(다시 읽어봐도 약간 애매하게 쓰인 듯...) 하면 안된다기보다는 이젠 pip install이 있어서 할 필요가 없어진 거라고 보면 된다.

그리고 README 문서를 보면 pip install --editable .로 설치하라고 되어 있다. pipPython 패키지 설치 도구인 것은 알고 있었는데 더 자세한 내용 확인을 위해서 Installing Python Modules를 참고했다.

  • pip는 현재 사용하는 인스톨 프로그램이고 Python 3.4부터는 Python에 포함되어 있다.
  • PyPI는 Python 패키지의 공개 저장소이다.
  • distutils은 1998년에 Python 표준 라이브러리로 추가된 빌드/배포 시스템이다. 현재 distutils을 직접 사용하는 게 점점 줄어들고 있지만, 현재 패키징 및 배포 인프라의 토대로 남아있다.

pip

기본 사용법은 pip install PackageName이다. 특정 버전을 설치하려면 pip install PackageName==1.0.4와 같이 버전일 지정할 수 있고 범위로 지정하려면 pip install PackageName>=1.0.4와 같이 사용할 수도 있다. 이미 패키지를 설치한 상황이라면 pip install --upgrade PackageName로 업데이트한다.

사용법은 파악했지만, 위에서 나온 대로 pip install --editable .를 할 때

  • 현재 폴더에서 설치할 때 setup.py와의 연관 관계를 아직 정확히 이해하지 못했다.
  • --editable의 용도를 이해하지 못했다.
  • pip install -r requirement.txt같은 명령어를 많이 본 적이 있는데 이 부분을 이해하지 못했다.
  • setup.pyrequirement.txt의 차이를 잘 모르겠다.

코드 한 줄 작성하기도 전에 모르는 게 너무 많아서 고생하고 있지만 하나씩 자료를 찾아봤다.

pip install .

pip install의 사용법을 살펴보자.

$ pip install --help

Usage:
  pip install [options] <requirement specifier> [package-index-options] ...
  pip install [options] -r <requirements file> [package-index-options] ...
  pip install [options] [-e] <vcs project url> ...
  pip install [options] [-e] <local project path> ...
  pip install [options] <archive url/path> ...

인자로 requirement specifier(아직 뭔지 모르겠지만), -r 옵션으로 requirements 파일, URL,디렉터리 경로를 지정할 수 있다고 나와 있다. 문서를 보면 로컬 디렉터리를 지정할 때 반드시 setup.py를 지정해야 한다고 되어 있다. 위에서 .은 현재 디렉터리를 지정한 것이므로 여기서 setup.py를 찾아서 자동으로 처리해 주는 것으로 보인다.

--editable

--editable 옵션의 문서를 보면 editable 모드로 프로젝트를 설치한다고 한다. Editable 설치 문서를 보면 setuptools develop 모드와 같다고 하고 로컬에 SomeProject.egg-info 디렉터리가 생기는데 setup.py develop보다 좋은 점은 현재 워킹 디렉터리에 바로 egg-info를 생성하는 부분이라고 한다. egg-info도 모르지만 setuptools develop 모드도 몰라서 문서를 읽어도 이해가 안 되었다.

이 내용은 Setuptools의 Development Mode에 자세히 나와 있었는데 이해한 대로만 간단히 줄여 보자면...

  • distutils가 기본적으로 프로젝트의 배포판을 빌드할 것이라고 가정하기 때문에 개발하면서 변경을 할 때 다시 빌드하고 설치해야 한다.
  • 동시에 두 가지 연관 프로젝트를 개발할 때 두 프로젝트를 한 디렉터리에 넣어서 실행해야 하는데 distutils로는 이를 할 수 없다.
  • Setuptools에서는 공통 디렉터리나 스테이징 영역에 파일을 복사하지 않은 채로 배포할 수 있도록 지원하고 있다. 이를 이용하면 각 프로젝트에서 코드를 바로 수정해서 사용할 수 있다. 이때는 C 확장이나 컴파일된 파일을 수정할 때만 빌드하면 된다.
  • setup.py develop 명령어를 사용하면 setup.py install과 비슷하게 동작하지만, 아무것도 설치하지 않는다. 대신 .egg-link를 생성하고 이를 프로젝트의 소스코드와 연결한다. 배포 디렉터리가 site-packages 디렉터리라면 소스코드를 포함하기 위해 easy-install.pth를 수정해서 sys.path에서 사용할 수 있게 한다.

아직 개발을 안 해봐서 의존성 설치와 현재 프로젝트의 코드 수정이 어떻게 연결되는지까지는 정확히 이해하지 못했지만 editable모드가 어떤 의미인지는 이해했다. 수정할 때마다 빌드하지 않게 한다는 용도로 이해했는데 이는 나중에 실제로 flask 애플리케이션을 개발하면서 겪어봐야 더 이해가 될 것 같다. --editable 옵션 없이 사용해보면 불편한 부분을 눈치채지 않을까 생각한다.

실제로 위 setup.py 파일로 pip install --editable .을 실행하면 아래의 파일이 새로 생긴다.

├── .eggs/
│   ├── README.txt
│   └── pytest_runner-2.12.1-py3.6.egg
├── demo.egg-info/
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── requires.txt
│   └── top_level.txt
├── venv/lib/python3.6/site-packages/easy-install.pth
├── venv/lib/python3.6/site-packages/demo.egg-link
└── venv/pip-selfcheck.json

이 파일 중에서 demo.egg-link를 보면 현재 폴더의 경로인 /Users/outsider/demo로 지정되어 있고 easy-install.pth도 같은 경로로 지정되어 있다.(지정되어 있다기보다 txt 파일이라 내용에 저 경로가 있다.) sys.path가 달라지진 않아서 정확한 동작 방식까지는 이해 못했지만 앞에서 이해한 내용대로 동작하기 위해서 만들어진 것 정도는 이해했다.

앞에서 Eggs 포맷에 대해서 간단히 살펴봤는데 실제로 관련 파일이 생기니까 .eggs.egg-info가 무엇인지 궁금해졌다. 이 부분은 The Internal Structure of Python Eggs에 자세히 나와 있었다.

  • .egg는 프로젝트의 코드와 리소스를 담고 있는 디렉터리나 zip 파일로 프로젝트의 메타데이터를 가진 EGG-INO 서브디렉토리 옆에 있는다.
  • .egg-info는 프로젝트의 코드와 리소스 옆에 있는 파일이나 디렉터리로 프로젝트의 메타데이터를 담고 있다.

.eggs 폴더 안에 setup_requires로 지정된 pytest_runneregg 파일이 있는 것으로 보아 설치한 내용이 여기에 담긴 것 같다. --editable 옵션이 없이 설치해보면 .eggs.egg-info가 생기지 않는 것으로 보아 Editable 모드로 실행하는 데 필요한 것으로 보인다. (아직 site-packages 안에 생긴 것과 .eggs 안에 생긴 것의 차이는 잘 모르겠다.)

이 파일들은 VCS에서 관리할 필요가 없으므로 .eggs/, *.egg-info/.gitignore 등으로 제외했다.

requirements.txt

Flask 예제 프로젝트를 보고 setup.py에 지정된 의존성을 이용해서 의존성을 설치했지만, 그동안 Python 프로젝트를 보면서는 pip install -r requirement.txt처럼 사용하는 것을 훨씬 더 많이 봤다. 그렇다 보니 이 둘의 차이가 궁금해졌고 의존성을 어느 쪽에서 관리하는 게 좋은지도 궁금해졌다.

pip의 Requirements Files를 보면 setup.py vs requirements.txt라는 글이 링크되어 있는데 다행히도 setup.py와 requirements.txt의 차이점과 사용 방법으로 번역이 되어 있다.

  • setup.py에는 PyPI에 배포할 라이브러리를 만들 때 의존성을 지정하고 requirements.txt는 서버 등에 배포할 때 의존성을 지정하는데 사용한다.
  • setup.py는 추상화된 의존성을 의미하고 requirements.txt는 실제 특정 라이브러리를 지정하는데 사용한다.
  • PyPI가 아닌 다른 곳에 패키지를 올려놓고 사용할 때 requirements.txt에서 받을 곳을 지정해서 사용할 수 있다.
  • 추상 의존성과 구체적 의존성을 나누어서 사용할 때의 좋은 점은 공개된 라이브러리를 수정해서 사용할 때 requirements.txt에서 다른 버전을 사용하도록 지정해서 사용할 수 있다.

이 글을 읽어보니 setup.pyrequirements.txt를 둘 다 사용하면서 의존성을 관리하고 개발자는 pip install -r requirements.txt로 설치해서 사용하도록 안내하는 게 좋은 방법이라고 생각되었다.(Flask 예제에서는 그렇게 하고 있지 않지만...) 일단 이렇게 관리하기 시작한 뒤에 필요한 경우 requirements.txt에서 다른 곳에서 받아오도록 지정할 수 있다.

pip freeze > requirements.txt 명령어를 이용해서 현재 로컬에 설치된 패키지 기준으로 requirements.txt를 만들어서 사용할 수 있지만 윗글에서 나온 대로 requirements.txt를 다음과 같이 만들어서 사용했다. PyPI에 올라온 라이브러리를 직접 수정해서 사용할 일은 근래에는 없을 것 같으므로 추가하는 의존성은 setup.py에서 지정해서 사용하면 될 것 같다.

$ cat requirements.txt
--index-url https://pypi.python.org/simple/

-e .

이를 이용해서 재설치를 해보자 정상적으로 잘 설치가 된다.

$ pip install -r requirements.txt
Obtaining file:///Users/outsider/demo (from -r requirements.txt (line 3))
Collecting flask (from demo==0.0.0->-r requirements.txt (line 3))
  Using cached Flask-0.12.2-py2.py3-none-any.whl
Collecting Jinja2>=2.4 (from flask->demo==0.0.0->-r requirements.txt (line 3))
  Using cached Jinja2-2.9.6-py2.py3-none-any.whl
Collecting Werkzeug>=0.7 (from flask->demo==0.0.0->-r requirements.txt (line 3))
  Using cached Werkzeug-0.12.2-py2.py3-none-any.whl
Collecting itsdangerous>=0.21 (from flask->demo==0.0.0->-r requirements.txt (line 3))
Collecting click>=2.0 (from flask->demo==0.0.0->-r requirements.txt (line 3))
  Using cached click-6.7-py2.py3-none-any.whl
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask->demo==0.0.0->-r requirements.txt (line 3))
Installing collected packages: MarkupSafe, Jinja2, Werkzeug, itsdangerous, click, flask, demo
  Running setup.py develop for demo
Successfully installed Jinja2-2.9.6 MarkupSafe-1.0 Werkzeug-0.12.2 click-6.7 flask-0.12.2 itsdangerous-0.24 demo

setup.cfg

예제 프로젝트인 flaskr에는 setup.cfg도 있었는데 이에 대해서는 Writing the Setup Configuration File에 나와 있었다.

[aliases]
test=pytest

setup.cfg의 내용은 위와 같았는데 위 문서를 보면 그 형태는 다음과 같다.

[command]
option=value

이는 커맨드라인 명령어를 사용할 때 해당 명령어에 옵션을 자동으로 제공하기 위해서 사용하거나 기본값을 제공하기 위해서 사용한다고 한다. 자세한 내용에 대해서는 잘 못 찾겠기는 한데 위 내용이나 Flask의 setup.cfg의 내용을 볼 때 setup.py에서 명령어를 실행할 때 참고정보를 여기에 넣어서 사용하는 것으로 보인다. 그래서 위에 별칭이 test=pytest로 되어 있는 것으로 보아 test 명령어를 실행하면 pytest를 실행하라는 정도의 의미로 보였다.

$ python setup.py test

running pytest
Searching for pytest
Best match: pytest 3.2.3
Processing pytest-3.2.3-py3.6.egg

Using /Users/outsider/demo/.eggs/pytest-3.2.3-py3.6.egg
Searching for py>=1.4.33
Best match: py 1.4.34
Processing py-1.4.34-py3.6.egg

Using /Users/outsider/demo/.eggs/py-1.4.34-py3.6.egg
running egg_info
writing demo.egg-info/PKG-INFO
writing dependency_links to demo.egg-info/dependency_links.txt
writing requirements to demo.egg-info/requires.txt
writing top-level names to demo.egg-info/top_level.txt
reading manifest file 'demo.egg-info/SOURCES.txt'
writing manifest file 'demo.egg-info/SOURCES.txt'
running build_ext
======================= test session starts =======================
platform darwin -- Python 3.6.3, pytest-3.2.3, py-1.4.34, pluggy-0.4.0
rootdir: /Users/outsider/demo, inifile:
collected 0 items

================== no tests ran in 0.00 seconds ===================

실제로 해보니 위 명령어로 pytest가 잘 실행되었고 python setup.py pytest로 실행해도 결과는 똑같았다.

MANIFEST.in

Flask 예제를 볼 때 용도를 몰라서 넘어갔던 파일이 MANIFEST.in이다. setup.pyinclude_package_data를 볼 때 여기서 이 파일에 지정한 디렉터리의 데이터 파일을 자동으로 포함한다고 하는데 정확한 용도는 모르겠다. 예시의 MANIFEST.in는 아래와 같이 되어 있다.

graft flaskr/templates
graft flaskr/static
include flaskr/schema.sql

이건 실제로 Flask 애플리케이션 개발을 해야 제대로 이해할 수 있을 것 같아서 일단은 개발환경에는 바로 넣지 않았다.

이제 Flask 애플리케이션 코드를 작성해 볼 수 있게 되었다.(한꺼번에 너무 많은 개념을 보느라 잘못된 부분도 있을 것 같지만...)


글을 쓴 이후로 SNS에서 피드백을 받은 내용이 있어서 추가로 내용을 보충합니다.(2017.10.09)

wheeleggs 관련:

  • wheel(*.whl)은 Eggs(*.egg)를 대체하려고 나온 것이고 현시점에서 거의 대체된 것으로 보인다고 한다. 그래도 setuptools의 내부에서는 아직 Eggs를 사용하는 것으로 보인다.
  • C/C++ 코드를 포함하는 패키지를 위한 포맷을 "bdist"라고 부른다. 빌드된 것을 배포하므로 설치할 때 C/C++ 컴파일러가 없어도 된다.
  • C/C++ 코드가 있더라도 소스 코드 형태로 압축해서 배포하는 것으로 "sdist"라고 부른다. 그래서 설치하는 쪽에서 C/C++ 컴파일러가 없으면 빌드할 수 없으므로 설치할 수 없다.
  • C/C++ 코드가 전혀 없으면 bdist, sdist 어느 쪽으로 배포해도 상관없다.
  • bdist 이름에는 OS, CPU 종류, libc 버전 등의 태그가 붙어서 pip로 설치할 때 부합되는 bdist를 선택해서 설치한다. libsass의 배포 파일을 보면 64비트 인텔 프로세스 Windows에서는 pip install libsass==0.13.2을 했을 때 해당 플랫폼용 bdsit가 있으므로 이를 받아서 압축을 푸는 것으로 설치가 끝나지만 FreeBSD에서 설치한다면 bdist가 없으므로 pip가 sdist를 다운받아서 C/C++ 코드를 직접 빌드하게 된다.

requirements.txtsetup.py 관련:

  • 둘 중 어느 쪽으로 패키징 할지 requirements.txt를 관리할 때 직접 할지 pip freeze로 할지는 아닌 많은 논의가 있다.(현재 정해진 방법은 없다.)
  • 파이썬 라이브러리를 만들면 setup.py를 쓰면 된다.
  • 실제로 해보면서도 라이브러리로 배포하는 게 아니라면 setup.py가 필요한가 하는 의문이 있었는데 실제로 관련 논의가 많이 있는 것 같고 웹 애플리케이션의 경우 requirements.txt로만 관리하기도 한다고 한다.
  • CLI 명령어를 제공해야 할 때에는 setup.py를 사용하는 이점이 있다.
  • setup.py는 일반 의존성 목록을 관리할 때 쓰고 requirements.txtpip freeze로 만들어서 lockfile처럼 쓰는 방법도 있다.

이건 개발해보면서 어느 쪽에 이점이 있는지 다양하게 시도해봐야 할 것 같다. 약속된 관례가 있는 것은 아니라는 것을 확인한 것도 큰 수확이다.(사실 이게 꽤 궁금했다.)

Flask 관련:

  • CLI로 Flask 앱을 실행하는 방식이 예전에는 없었던 방식이라는 걸 알았다.
  • Flask 문서를 보면 어디서는 flask CLI로 실행하고 어느 문서에서는 if __name__ == '__main__': app.run()같은 코드를 하단에 두어서 python hello.py로 실행하고 있다. 대문에서 CLI로 안내하고 있어서 이 글은 그대로 적었는데 후자의 방법이 감춰진 마법이 적어서 이해하기 좋을 것이라고 한다.
2017/10/09 01:45 2017/10/09 01:45

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