Outsider's Dev Story

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

기술 뉴스 #88 : 17-10-15

웹개발 관련

  • Lessons from migrating a large codebase to React 16 : Discord에서 React로 만들어진 코드를 React 16 기반으로 변경한 과정을 설명한 글이다. 먼저 codemod를 이용해서 코드를 수정하고 수동으로 빠진 부분을 수정하는 방식으로 적용했다. Discord에서는 React.PropTypes, React.createClass의 사용과 React 내부 API를 직접 사용하는 부분이 주요 문제가 되었다고 한다.(영어)
  • React v16 무엇이 바뀌었나 : React.js의 새 메이저 버전인 v16의 바뀐 점을 정리한 글이다. 내부적으로는 React Fiber가 도입해서 성능 개선을 한 게 중요한 차이이고 render()에서 배열을 반환할 수 있고 에러 핸들링이 추가된 게 주목할 부분이라고 생각한다.(한국어)
  • JavaScript's Promise Leaks Memory : JavaScript의 Promise에 구조적인 메모리 누출 문제를 지적한 글이다. Promise/A+ 스펙을 따른 것이므로 구현체에 누출이 있는 것은 당연하다고 보통 얘기하는데 여기에 동의하지 않는다는 논지의 글이다. 예제 코드를 돌리면 현재 Node.js와 Chrome에서 모두 문제가 생기는데 Bluebird 같은 구현체나 다른 언어나 라이브러리의 구현체는 실제로 이런 문제가 있지 않으므로 이 부분이 차후 문제가 될 수 있다는 얘기를 하고 있다. 네이티브 Promise에 메모리 누수가 있는지도 모르고 있었다.(영어)
  • Asynchronous stack traces: why await beats .then() : JavaScript에서 비동기를 쓸 때 Promise를 이용한 .then() 체인이 있고 await를 이용해서 Promise를 사용할 수 있다. 후자가 가독성이 훨씬 좋지만, 그 이상으로 스택트레이스를 유지하기 위해 전자의 경우 JS 엔진이 스택 정보를 저장하고 있어서 메모리를 차지하지만 await는 실제 함수의 실행이 멈춰있으므로 별도로 유지할 필요가 없다고 한다.(영어)

그 밖의 프로그래밍 관련

  • 파이썬의 개발 "환경"(env) 도구들 : Python 개발환경을 구축할 때 사용하는 pyenv, virtualenv, virtualenvwrapper 등의 도구가 어떤 역사 속에서 만들어졌고 각 도구의 용도가 어떻게 다른지 비교하면서 정리한 글이다. Python에 이런 도구들이 많이 있어서 원하는 것을 찾기 힘들었는데 이 글을 통해서 이해할 수 있고 마지막에 용도별로 어떤 도구를 쓰는지 추천가지 해주고 있다.(한국어)
  • Mitigating replication lag and reducing read load with freno : GitHub에서 MySQL을 master-replica 형태로 사용하면서 리플리카와의 데이터 불일치를 해결하기 위해서 큰 작업을 나누어서 처리하고 쓰로틀러를 도입해서 사용했다. 이는 Ruby로 만들어져 있었는데 이후 시스템이 커지면서 Perl 쓰로틀러를 만들었지만 두 구현체간의 퀄리티 차이도 존재했고 시스템이 더 커지면서 freno라는 별도의 쓰로틀러 서비스를 만들었다. 이 시스템으로 전체 시스템의 쓰로틀링을 안정적으로 제공하면서 각 리플리카의 지연시간의 매트릭을 수집해서 각 시스템이 이를 확인하면서 데이터를 처리할 수 있게 되었다. 이전에는 데이터 불일치를 해결하기 위해서 일정시간 이내에 데이터가 작성되었다면 master에 확인해서 데이터불일치 여부를 검사했는데 freno를 이용하면서는 replica의 지연시간을 알 수 있으므로 master에 읽기 요청을 하는 시간을 많이 줄일 수 있게 되었다. 각 문제의 해결 과정과 최종 솔루션인 freno의 도입효과까지 아주 잘 정리되어 있다.(영어)
  • Stretching Spokes : GitHub에서 git 저장소의 리플리카를 만드는 데 사용하는 Spokes를 설명한 글이다. 이전에는 DRBD를 쓰고 있었는데 레이턴시에 민감해서 가까운 곳에 둘 수밖에 없었지만, Spokes를 이용해서 완전히 다른 지역에도 리플리카를 만들 수 있게 되었고 이를 구현하기 위해 git 참조 업데이트의 트랜잭션을 구현하고 Spokes 체크섬으로 리플리카가 이뤄지는 과정이 보장되도록 구현한 접근방법을 설명한 글이다.(영어)
  • JUnit 5 소개 : 최근에 나온 JUnit 5가 4와 달라진 점을 바탕으로 구조를 설명하고 Junit 5를 구성해서 사용하는 방법을 설명한다.(한국어)
  • GitHub 프로젝트의 의존성 정보를 알려주는 기능 추가 : GitHub에서 자체적으로 프로젝트가 사용하는 의존성 정보를 정리해서 보여주고 해당 프로젝트를 사용 중인 저장소를 보여줍니다. 최근에 GitHub과 연동해서 의존성 라이브러리를 관리할 수 있는 서비스들라는 글을 올렸는데 GitHub에서 자체적으로 각 라이브러리의 보안 이슈를 알려주는 "Security alerts" 기능도 오픈할 것이라고 한다.(한국어)
  • Vim 배우는 법: 4주 계획 : How To Learn Vim: A Four Week Plan의 번역 글로 vimtutor로 연습한 뒤에 플러그인을 설치 않은 Vim으로 연습해보면서 익히는 4주간의 단계별 과정이 잘 나와 있다.(한국어)
  • 타입과 타입 시스템 : 연재를 시작하며 : 프로그래밍 언어에서 항상 논의되는 타입과 관련된 연재의 시작 글로 이 글에서는 타입/타입 시스템의 기본 개념을 설명하고 있다. 아직 첫 글만 올라왔는데 자세히 알지 못하는 영역이라 이후 내용이 기대되어 목록에 추가해 놓았다.(한국어)

볼만한 링크

  • 경영자와 직원이 서로를 바보로 만드는 조직: 발단은 리더가 심은 두려움 : 회사에서 리더가 직원들에게 두려움을 심어줌으로써 리더는 상황에서 고립되고 판단을 제대로 못 하게 되고 직원도 그런 얘기를 점점 안 하게 되는 악순환이 되는 과정을 설명한 칼럼이다. 이렇게 진행되는 걸 현실에서 너무 많이 보고 들어서인지 잘 정리된 글에 공감을 많이 했다.(한국어)
  • 강남 출근길에 판교/정자역에 내릴 사람 예측하기 : 지하철에서 앉아서 가기 위해 내릴 사람을 예측하려고 사람들의 특징을 분류한 뒤 데이터를 분석한 결과를 정리한 글이다. 아주 장난스러우면서도 내용을 보면 상당히 치밀하게 데이터를 구축하고 분석해서 비교해볼 결과에 웃음이 나면서 흥미롭게 볼 수 있는 발표자료다.(한국어)
  • WeWork, 1년 후 : Shakr에서 WeWork에 입주하고 1년이 지난 시점에서 내부의 사진을 포함해서 그동안의 경험을 공유한 글이다. 라운지까지는 가봤지만, 내부는 못 봤었고 실제 사용 경험이 어떨지도 궁금했는데(가격은 꽤 비싸다고 들어서) 전체적으로 만족한다는 부분에서 더 흥미가 갔다.(한국어)

IT 업계 뉴스

프로젝트

  • GraphQL::SchemaComparator : GraphQL 스키마 2개를 비교해서 차이점을 출력해 주는 Ruby 라이브러리
  • Matcha : Go 언어로 iOS/Android 앱을 만드는 패키지.
  • ZeroNet : Bitcoin 암호화와 BitTorrent 네트워크로 분산 웹사이트를 운영할 수 있는 프로그램. 웹사이트에 접속하려면 ZeroNet 프로그램이 실행되어 있어야 한다.
  • Quick : Swift/Objective-C BDD 테스트 프레임워크.
  • OpenRCT2 : 롤러코스터 타이쿤 2의 오픈소스 버전.
  • security.txt : 사이트의 보안 관련 정보를 담는 security.txt에 대한 웹사이트로 현재 RFC의 드래프트 상태라고 한다.

버전 업데이트

2017/10/15 16:47 2017/10/15 16:47

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