Outsider's Dev Story

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

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