Outsider's Dev Story: Python 카테고리 글 목록https://blog.outsider.ne.kr/Stay Hungry. Stay Foolish. Don't Be Satisfied.2024-03-15T09:46:16+09:00Textcube 1.10.7 : Tempo primoFlask 공식 튜토리얼 따라하기 #3Outsiderhttps://blog.outsider.ne.kr/13502018-02-12T02:53:55+09:002018-02-12T02:53:55+09:00<p><a href="https://blog.outsider.ne.kr/1343">Flask 공식 튜토리얼 따라하기 #2</a>에 이어진 글이다.<br />
<br><br></p>
<h1><a href="http://flask.pocoo.org/docs/0.12/tutorial/dbcon/#tutorial-dbcon">4 단계: 데이터베이스 연결</a></h1>
<p>앞에서 <code>connect_db</code>로 데이터베이스 연결을 구성하는 함수를 만들었지만 유용하지는 않다. 왜냐하면, 매번 데이터베이스를 연결하고 종료하는 것은 아주 비효율적이라서 연결을 더 오래 유지할 필요가 있다. 데이터베이스 연결은 하나의 트랜잭션을 가지므로 한 요청이 한 번에 하나의 연결을 사용함을 보장해야 한다. 이렇게 하는 좋은 방법은 애플리케이션 컨텍스트를 유틸라이징하는 것이다.</p>
<p>Flask에는 애플리케이션 컨텍스트와 리퀘스트 컨텍스트가 있지만 여기서는 이 컨텍스트를 사용할 특수 변수가 있다는 거만 알면 된다고 한다. <code>request</code> 변수는 현재 요청에 관한 요청 객체이고 <code>g</code>는 현재의 애플리케이션 컨텍스트와 연관된 범용 변수이다. 그냥 변수가 있는 거구나 싶지만, 뒤에서 더 살펴본다고 하니까 넘어가자.</p>
<p>지금은 <code>g</code> 객체에 정보를 안전하게 저장할 수 있다는 것을 알면 된다고 한다. 애플리케이션 컨텍스트의 범위를 정확히 몰라서 여기에 어떤 정보를 담아야 안전한지 잘 모르겠지만 이것도 일단 넘어갔다. 이 변수에 헬퍼 함수를 만들어서 처음 호출되었을 때 현재 컨텍스트에 데이터베이스 연결을 만들고 이후의 호출에서는 이미 생성한 연결을 반환해서 사용하겠다고 한다. 이 함수가 <code>get_db()</code>이고 나는 <code>flaskr/flaskr.py</code>에 이 함수를 넣었다.</p>
<pre class="line-numbers"><code class="language-python">def get_db():
"""Opens a new database connection if there is none yet for the
current application context.
"""
if not hasattr(g, 'sqlite_db'):
g.sqlite_db = connect_db()
return g.sqlite_db
</code></pre>
<p>이 함수로 디비에 접속할 수 있게 되었다. 반대인 접속해제는 Flask에서 <code>teardown_appcontext()</code> 데코레이터를 제공하고 있어서 애플리케이션 컨텍스트가 내려갈 때마다 실행된다고 한다. 이는 아래와 같이 선언할 수 있다.</p>
<pre class="line-numbers"><code class="language-python">@app.teardown_appcontext
def close_db(error):
"""Closes the database again at the end of the request."""
if hasattr(g, 'sqlite_db'):
g.sqlite_db.close()
</code></pre>
<p>튜토리얼 설명에 따르면 애플리케이션 컨텍스트는 요청이 들어오기 전에 만들어지고 요청이 끝날 때마다 제거된다고 한다. 애플리케이션 컨텍스트라는 이름도 그렇고 데이터베이스 접속을 끊는 과정을 여기에 넣어서 당연히 여기서 teardown은 앱이 내려갈 때를 의미하는 중 알았는데 요청마다 실행된다고 해서 실제로 로그를 출력해보니 정말 요청마다 teardown이 실행되었다.</p>
<pre class="line-numbers"><code class="language-bash">tear down
127.0.0.1 - - [29/Jan/2018 04:14:51] "GET / HTTP/1.1" 404 -
tear down
127.0.0.1 - - [29/Jan/2018 04:14:51] "GET /favicon.ico HTTP/1.1" 404 -
</code></pre>
<p>자세한 내용은 <a href="http://flask.pocoo.org/docs/0.12/appcontext/#app-context">Application Context 문서</a>를 살펴보라고 한다.<br />
<br></p>
<h2>애플리케이션 컨텍스트</h2>
<p><a href="http://flask.pocoo.org/docs/0.12/appcontext/#app-context">Application Context</a>를 살펴보라니까 자세히 좀 보기로 했다.</p>
<p>코드를 실행할 때 두 가지 상태가 존재하는 것이 Flask 설계 개념 중 하나다. 한 상태는 애플리케이션 설정 상태(application setup state)인데 Flask 객체가 인스턴스화 될 때 시작되고 첫 요청이 들어오면 자연히 종료되는데 이 상태에서는 다음 내용이 참이라고 가정한다.</p>
<ul>
<li>프로그래머는 애플리케이션 객체를 안전하게 수정할 수 있다.</li>
<li>아직 요청을 처리하지 않았다.</li>
<li>애플리케이션 객체를 수정하려면 참조해야 하는데 이 객체의 참조를 제공하는 다른 특별한 프락시는 존재하지 않는다.</li>
</ul>
<p>반면 요청을 처리하는 중에는 다음 내용을 참이라고 가정할 수 있다고 하는데 이 요청을 처리하는 것을 또 하나의 상태로 정의하는 것으로 보인다.</p>
<ul>
<li>요청 처리 중에는 컨텍스트 로컬 객체(context local object, <code>flask.request</code> 등)가 현재 요청을 가리키고 있다.</li>
<li>언제 어떤 코드든 간에 이 객체를 가질 수 있다.</li>
</ul>
<p>이 외에 세 번째 상태도 있는데 요청을 처리하는 중 애플리케이션과 상호작용하는 방법과 비슷하게 요청이 없는 상태에서도 애플리케이션을 다루는 경우가 있다. 인터렉티브 Python 셸에서 애플리케이션과 상호작용 하는 명령행 애플리케이션을 예로 들 수 있다. 애플리케이션 컨텍스트가 <code>current_app</code> 컨텍스트 로컬을 강력하게 만드는 것이다. 이 부분은 나중에 앱을 작성하다 보면 제대로 이해할 수 있을 것 같다.<br />
<br></p>
<h3>애플리케이션 컨텍스트의 목적</h3>
<p>애플리케이션 컨텍스트가 존재하게 된 주요 이유는 과거 요청 컨텍스트에 여러 기능을 추가하려는데 더 나은 해결책이 없었기 때문이다. 이는 한 Python 프로세스에서 여러 애플리케이션을 사용할 수 있다는 Flask의 설계 원칙 중 하나 때문인데 이 상황에서 코드가 "올바른" 애플리케이션을 찾는 방법이 문제였다. 예전에는 애플리케이션을 명시적으로 전달하기를 권했는데 이를 고려하지 않고 만든 라이브러리에서 문제가 생겨서 현재 요청의 애플리케이션 참조에 바인딩 된 <code>current_app</code> 프락시를 나중에 사용하게 하는 것이 일반적인 해결책이었다고 한다.<br />
<br></p>
<h3>애플리케이션 컨텍스트 생성</h3>
<p>애플리케이션 컨텍스트를 생성하는 데는 두 가지 방법이 있다. 첫 번째 방법은 암묵적인 방법인데 요청 컨텍스트가 들어올 때마다 필요하다면 애플리케이션 컨텍스트도 생성한다. 그래서 필요하지 않다면 애플리케이션 컨텍스트의 존재는 무시해도 된다. <code>app_context()</code> 메서드를 명시적으로 사용하는 것이 두 번째 방법이다.</p>
<pre class="line-numbers"><code class="language-clike">from flask import Flask, current_app
app = Flask(__name__)
with app.app_context():
# 이 블록내에서는 current_app는 앱을 가리킨다.
print current_app.name
</code></pre>
<p><code>SERVER_NAME</code>이 설정된 경우 애플리케이션 컨텍스트는 <code>url_for()</code> 함수에서도 사용되므로 요청이 없더라도 URL을 생성할 수 있다. 요청 컨텍스트가 들어오지 않고 애플리케이션 컨텍스트를 명시적으로 설정하지 않았다면 <code>RuntimeError</code>가 발생할 것이다.</p>
<pre class="line-numbers"><code class="language-clike">RuntimeError: Working outside of application context.
</code></pre>
<p><br></p>
<h3>컨텍스트의 범위</h3>
<p>애플리케이션 컨텍스트는 필요에 따라 생성되고 제거되며 스레드 간에 이동하지 않고 요청 간에 공유되지도 않는다. 그래서 데이터베이스 연결 정보 등을 저장하기에 안성맞춤이다. 내부 스택 객체를 <code>flask._app_ctx_stack</code>라고 부른다.</p>
<p>확장 기능도 최상위 수준에 추가적인 정보를 맘껏 저장할 수 있는데 이때 사용자 코드가 예약한 <code>flask.g</code> 객체 대신 충분히 구별되는 이름을 사용해서 정보를 저장한다고 가정한다. 더 자세한 내용은 <a href="http://flask.pocoo.org/docs/0.12/extensiondev/#extension-dev">Flask 확장 개발</a>을 참고하라고 한다.<br />
<br></p>
<h3>컨텍스트 사용 방법</h3>
<p>요청당 생성되어야 하는 리소스를 캐시할 때 보통 컨텍스트를 사용하는데 데이터베이스 연결이 대표적이다. 컨텍스트가 Flask 애플리케이션과 확장 간에 공유되므로 애플리케이션 컨텍스트에 저장할 때 유일한 이름을 선택하는 것이 필수다. 가장 일반적인 사용방법은 리소스 관리를 두 가지로 나누는 것이다.</p>
<ol>
<li>컨텍스트의 캐싱된 리소스</li>
<li>리소스 할당 해제에 기반을 둔 컨텍스트 제거</li>
</ol>
<p>보통 존재하지 않으면 리소스 X를 생성하고 존재하면 같은 리소스를 반환하는 <code>get_X()</code> 함수와 teardown 핸들러로 등록된 <code>teardown_X()</code> 함수가 있을 수 있다.</p>
<pre class="line-numbers"><code class="language-clike">import sqlite3
from flask import g
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = connect_to_database()
return db
@app.teardown_appcontext
def teardown_db(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
</code></pre>
<p>처음 <code>get_db()</code>가 호출되면 연결이 이뤄지고 암묵적으로 이를 사용하려고 <code>LocalProxy</code>를 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-clike">from werkzeug.local import LocalProxy
db = LocalProxy(get_db)
</code></pre>
<p>이 방법을 이용하면 내부적으로 <code>get_db()</code>를 호출하면서 데이터베이스에 직접 접근할 수 있다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1350?commentInput=true#entry1350WriteComment">댓글 쓰기</a></strong></p>Flask 공식 튜토리얼 따라하기 #2Outsiderhttps://blog.outsider.ne.kr/13432018-01-22T22:13:15+09:002018-01-22T22:13:15+09:00<p><a href="https://blog.outsider.ne.kr/1329">Flask 공식 튜토리얼 따라하기 #1</a>에 이어서 Flask 튜토리얼을 따라해 보고 있다.(2편이 너무 늦었지만....)<br />
<br><br><br></p>
<h1><a href="http://flask.pocoo.org/docs/0.12/tutorial/packaging/#tutorial-packaging">3 단계: flaskr를 패키지로 설치하기</a></h1>
<p>앞에서 계속 이해 못 하고 있던 flaskr을 패키지로 설명하는 부분이 나왔다. Flask에는 <a href="http://click.pocoo.org/">Click</a> 패키지가 포함되어 있어서 Flask 커맨드라인 도구를 제공한다고 한다. Click을 보면 CLI 프로그램을 쉽게 만들게 도와주는 패키지인 것으로 보이는데 <code>flask</code> 커맨드 라인은 튜토리얼 뒤에서 더 나온다고 하므로 일단 그냥 넘어갔다. Click은 나중에 CLI를 만들 때 보면 될 것 같다.</p>
<p>Flask 애플리케이션을 관리할 때 <a href="https://packaging.python.org/">Python 패키지 가이드</a>를 따르는 게 좋다고 한다. 일단 이건 뒤에서 더 보기로 하고 일단 프로젝트 루트에 <code>setup.py</code>와 <code>MANIFEST.in</code> 파일을 만든다.</p>
<pre class="line-numbers"><code class="language-python">from setuptools import setup
setup(
name='flaskr',
packages=['flaskr'],
include_package_data=True,
install_requires=[
'flask',
],
)
</code></pre>
<p><code>setup.py</code>의 내용이다. <a href="https://blog.outsider.ne.kr/1325">Flask 애플리케이션 개발 환경 구성</a>에서도 찾아봤지만, 프로젝트 정보 및 의존성 등을 명시하고 있는 파일이다. <code>name</code>은 이 패키지의 이름이고 <code>packages</code>에 <code>flaskr</code>라고 지정했으므로 모듈을 찾기 위해 <code>flaskr/__init__.py</code>를 찾게 된다. <code>include_package_data</code>를 <code>True</code>라고 했으므로 <code>MANIFEST.in</code>에서 명시한 패키지 디렉터리의 데이터 파일을 포함하게 되고 여기서는 <code>flask</code>가 필요하므로 <code>install_requires</code>에 지정했다.</p>
<pre class="line-numbers"><code class="language-clike">graft flaskr/templates
graft flaskr/static
include flaskr/schema.sql
</code></pre>
<p>위는 <code>MANIFEST.in</code> 파일의 내용이다. setuptools를 사용할 때 패키지에 포함하기 위한 특수 파일들을 여기에 지정해야 한다.</p>
<pre class="line-numbers"><code class="language-python">from .flaskr import app
</code></pre>
<p><code>flaskr/</code> 아래 <code>__init__.py</code> 파일을 위 내용으로 만든다. 이 import 문을 쓰면 Falskr 애플리케이션 인스턴스는 패키지의 최상위로 가져올 수 있다고 한다. Flask 개발 서버가 이 인스턴스의 위치를 알아야 하는데 이 import 문이 없으면 <code>FLASK_APP=flaskr.flaskr</code>같은 환경변수를 지정해 주어야 한다.</p>
<p>이제 <code>pip</code>로 패키지를 설치한다.</p>
<pre class="line-numbers"><code class="language-bash">$ pip install --editable .
Obtaining file:///Users/outsider/temp/flaskr
Collecting flask (from flaskr==0.0.0)
Using cached Flask-0.12.2-py2.py3-none-any.whl
Collecting Werkzeug>=0.7 (from flask->flaskr==0.0.0)
Using cached Werkzeug-0.12.2-py2.py3-none-any.whl
Collecting Jinja2>=2.4 (from flask->flaskr==0.0.0)
Using cached Jinja2-2.9.6-py2.py3-none-any.whl
Collecting itsdangerous>=0.21 (from flask->flaskr==0.0.0)
Collecting click>=2.0 (from flask->flaskr==0.0.0)
Using cached click-6.7-py2.py3-none-any.whl
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask->flaskr==0.0.0)
Installing collected packages: Werkzeug, MarkupSafe, Jinja2, itsdangerous, click, flask, flaskr
Running setup.py develop for flaskr
Successfully installed Jinja2-2.9.6 MarkupSafe-1.0 Werkzeug-0.12.2 click-6.7 flask-0.12.2 flaskr itsdangerous-0.24
</code></pre>
<p>이제 실행하면 서버가 잘 뜬다. 물론 아직 라우트 설정이 없으므로 404 밖에 나오지 않는다.</p>
<pre class="line-numbers"><code class="language-bash">$ FLASK_APP=flaskr FLASK_DEBUG=true flask run
* Serving Flask app "flaskr"
* Forcing debug mode on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 351-985-958
</code></pre>
<p>여기까지 따라 했는데도 궁금한 게 많이 생겼다.<br />
<br></p>
<h2><code>MANIFEST.in</code></h2>
<p><code>MANIFEST.in</code>에 패키지에 필요한 폴더를 지정한 건 이해했는데 정확한 동작은 알지 못했다. <a href="https://docs.python.org/3.6/distutils/sourcedist.html#the-manifest-in-template">소스배포판(<code>sdist</code>)를 만드는 방법</a>을 살펴보면 <code>sdist</code> 명령어가 다음 파일들을 기본적으로 패키지에 포함한다.</p>
<ul>
<li><code>py_modules</code>와 <code>packages</code> 옵션이 암시하는 모든 Python 소스 파일.</li>
<li><code>ext_modules</code>와 <code>libraries</code>에서 지정한 모든 C 소스 파일.</li>
<li><code>scripts</code> 옵션으로 구분할 수 있는 스크립트.</li>
<li><code>test/test*.py</code> 처럼 테스트 스크립트로 보이는 모든 것.(현재는 소스배포판에서 포함하는 것 외에 Distutils가 아무 일도 하지 않지만, 차후에 Python 모듈 배포판을 테스트하는 것이 표준이 될 것이다.)</li>
<li><code>README.txt</code>(또는 <code>README</code>), <code>setup.py</code>, <code>setup.cfg</code>.</li>
<li><code>package_data</code> 메타데이터와 일치하는 모든 파일.</li>
<li><code>data_files</code> 메타데이터와 일치하는 모든 파일.</li>
</ul>
<p>이 외에 파일을 포함하는 방법이 <code>MANIFEST.in</code>라는 매니페스트 템플릿을 작성하는 것이다. 이 파일에서 소스 배포판에 포함할 파일목록을 작성하면 <code>sdist</code> 명령어가 이 템플릿 파일에 나온 대로 처리해서 파일을 찾는다.</p>
<pre class="line-numbers"><code class="language-clike">graft flaskr/templates
graft flaskr/static
include flaskr/schema.sql
</code></pre>
<p>그래서 여기서 보면 Flask의 정적 파일과 뷰파일 및 SQL 파일을 배포판에 포함하기 위한 설정임을 알 수 있다. <code>MANIFEST.in</code>에 나온 각 명령어는 <a href="https://docs.python.org/3.6/distutils/commandref.html?highlight=graft"><code>sdist</code>의 명령어</a>인데 <code>graft dir</code>는 디렉터리 밑의 모든 파일을 포함하라는 의미이고 <code>include pattern1 pattern2 ...</code>는 패턴과 일치하는 모든 파일을 포함하라는 의미이다.<br />
<br></p>
<h2><code>__init__.py</code></h2>
<p><code>__init__.py</code>를 쓰면 패키지로 인식한다는데 정확히 어떤 역할을 하는 파일인지가 궁금했다. <a href="https://docs.python.org/3/tutorial/modules.html#packages">Python 문서</a>를 보면 다음과 같이 나와 있다.</p>
<blockquote>
<p>Python이 디렉터리가 패키지를 담고 있는 것처럼 다루려면 <code>__init__.py</code> 파일이 필요하다. <code>string</code>같은 일반적인 이름으로 된 폴더가 유효한 모듈인데 의도치 않게 모듈 검색 경로에서 의도치 않게 숨겨지는 일을 막는 데 필요하다. 가장 간단한 상황에서는 아무 내용도 없는 <code>__init__.py</code>를 만들어도 되지만 이 파일에서 패키지의 초기화 코드를 실행하거나 <code>__all__</code> 변수를 설정할 수 있다.</p>
</blockquote>
<p>일반 디렉터리와 Python이 패키지로 다룰 폴더의 구분을 위해 필요하다고 이해했고 초기화 코드를 실행한다는 걸 보니 패키지를 <code>import</code> 할 때 실행되는 것으로 보인다.</p>
<p>그래서 <code>string/__init__.py</code>라는 파일을 만들고</p>
<pre class="line-numbers"><code class="language-python">print('This is a string package')
</code></pre>
<p>루트 디렉터리에서 REPL로 테스트를 해보았다.</p>
<pre class="line-numbers"><code class="language-bash">$ python
Python 3.6.3 (default, Oct 7 2017, 13:45:53)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.37)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import string
This is a string package
>>> import string
</code></pre>
<p><code>string</code>이라는 모듈을 잘 가져오고 <code>__init__.py</code>가 실행되는 것을 볼 수 있다. 당연히 임포트 할 때마다 실행되는 것은 아니므로 내부에서 캐싱하는 것으로 보인다.</p>
<p>여기까지 해보자 Python 모듈과 <code>import</code>와 관련해서 어떻게 동작하는지 좀 더 궁금해져서 다음과 같이 테스트를 위한 파일과 폴더를 생성했다.</p>
<pre class="line-numbers"><code class="language-bash">├── foo.py
├── bar.py
├── baz
│ └── bar.py
├── qux
│ ├── __init__.py
│ └── bar.py
└── corge
├── __init__.py
└── bar.py
</code></pre>
<p><code>foo.py</code>와 <code>bar.py</code>는 파일을 임포트 할 때는 테스트해보기 위함이고 <code>baz</code>는 <code>__init__.py</code>가 없는 폴더, qux는 빈 <code>__init__.py</code>를 가진 폴더, <code>corge</code>는 내용이 있는 <code>__init__.py</code>가 있는 폴더다. 각 파일의 내용은 아래 한꺼번에 적어두었다.</p>
<pre class="line-numbers"><code class="language-python"># foo.py
print('This is a foo.py')
# bar.py
def hello():
print('Hello World')
</code></pre>
<p>REPL로 다시 테스트를 해봤다.</p>
<pre class="line-numbers"><code class="language-bash">$ python
Python 3.6.3 (default, Oct 7 2017, 13:45:53)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.37)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import foo
This is a foo.py
>>> foo
<module 'foo' from '/Users/outsider/python-test/foo.py'>
</code></pre>
<p>그냥 파일인데 잘 임포트가 된다.</p>
<pre class="line-numbers"><code class="language-bash">>>> import bar
>>> bar
<module 'bar' from '/Users/outsider/python-test/bar.py'>
>>> bar.hello()
Hello World
</code></pre>
<p>함수가 있는 파일을 임포트하면 모듈에서 함수를 실행할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">>>> import baz
>>> baz
<module 'baz' (namespace)>
>>> baz.bar
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'baz' has no attribute 'bar'
>>> baz.hello()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'baz' has no attribute 'hello'
>>> import baz.bar
>>> baz.bar.hello()
Hello World
</code></pre>
<p><code>__init__.py</code>가 없는 폴더도 임포트는 할 수 있는데 따로 그 안에 내용에 접근할 수는 없는 것 같다. 대신 파일을 불러오듯이 폴더 구조에 따라 <code>import baz.bar</code>처럼 하는 건 가능하다.</p>
<pre class="line-numbers"><code class="language-python">>>> import qux
>>> qux
<module 'qux' from '/Users/outsider/python-test/qux/__init__.py'>
>>> qux.bar
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'qux' has no attribute 'bar'
>>> qux.hello()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'qux' has no attribute 'hello'
>>> import qux.bar
>>> qux.bar.hello()
Hello World
</code></pre>
<p>빈 <code>__init__.py</code>가 있는 경우도 위와 같이 동작한다. 다면 임포트한 모듈을 출력했을 때 앞에서는 <code><module 'baz' (namespace)></code>처럼 나왔는데 이번에는 <code><module 'qux' from '/Users/outsider/python-test/qux/__init__.py'></code>로 나왔다. <a href="https://docs.python.org/3/reference/import.html#packages">Python 임포트 문서</a>를 보면 Python에는 regular packages와 namespace packages 두 가지 타입이 있다고 한다. <a href="https://docs.python.org/3/reference/import.html#regular-packages">regular packages</a>는 Python 3.2 이전부터 존재하던 전통적인 패키지로 <code>__init__.py</code>가 있는 폴더를 의미하고 임포트했을 때 <code>__init__.py</code>를 자동으로 실행하고 그 객체를 패키지 네임스페이스에 바인딩한다.</p>
<p><a href="https://docs.python.org/3/reference/import.html#namespace-packages">namespace packages</a>는 완전히 이해는 못 했는데 <code>__init__.py</code>가 없는 패키지를 의미하고 그 안의 파일들은 꼭 파일 시스템에 있어야 하는 것은 아니고 네트워크나 zip 파일 등에 있을 수도 있다고 한다. 좀 더 동적으로 다양한 경우를 해결할 수 있도록 새로 추가된 패키지 타입이 아닌가 싶다. 조만간 쓸 일은 없어 보여서 그냥 넘어갔다.</p>
<pre class="line-numbers"><code class="language-python"># corge/__init__.py
from .bar import hello
</code></pre>
<p><code>corge/__init__.py</code>는 위의 내용으로 작성했다. 이렇게 작성하고 임포트하자 아래처럼 바로 <code>hello</code> 변수를 사용할 수 있었다.</p>
<pre class="line-numbers"><code class="language-bash">>>> import corge
>>> corge
<module 'corge' from '/Users/outsider/python-test/corge/__init__.py'>
>>> corge.hello()
Hello World
</code></pre>
<p>실제 모듈을 작성해 봐야 하겠지만 대충의 동작은 이해한 것 같다.</p>
<p>길게 돌아왔지만, 다시 튜토리얼 코드를 보면 <code>flaskr/__init__.py</code>를 <code>from .flaskr import app</code>로 작성했으므로 <code>flaskr/flaskr.py</code>에 있는 <code>app</code>을 모듈로 외부에 노출해서 <code>import flaskr</code>을 했을 때 <code>flaskr.app</code>으로 바로 사용하게 하려는 의도임을 추측할 수 있었다. 실제로 튜토리얼에서는 이 코드가 있으면 <code>export FLASK_APP=flaskr.flaskr</code>로 할 필요가 없다고 나오는데 실제 실행은 <code>export FLASK_APP=flaskr</code>로 지정해서 하고 있다. 조금 전 테스트를 통해서 <code>flaskr.flaskr</code>로 시작 파일을 찾지 않고 왜 <code>flaskr</code>만으로 찾을 수 있는지 알 수 있게 되었다.<br />
<br></p>
<h2><a href="https://packaging.python.org/">Python 패키지 가이드</a></h2>
<p>Python에서 아직 가장 이해가 안 되는 부분 중 하나인데 Flask 애플리케이션을 관리할 때 <a href="https://packaging.python.org/">Python 패키지 가이드</a>를 따르는 게 좋다고 한다. 실제로 튜토리얼에서도 예제를 작성한 뒤에 <code>pip</code>로 설치해서 실행하고 있다. PyPI에 배포하고 다른 프로젝트에서 가져다 쓰는 모듈의 경우에는 패키징과 설치가 필요하다는 것을 이해하는데 Flask같은 웹 애플리케이션 같은 경우에도 왜 패키징과 설치가 필요한지 이해할 수가 없었다. 보통 이런 코드는 서버에 배포해서 바로 실행을 하거나(<a href="http://flask.pocoo.org/">Flask 홈페이지에 나온 것처럼</a>) 소스를 배포해서 어떤 환경에서 실행하도록 하는 것이지 다른 프로젝트가 가져다 쓰는 목적이 아니라고 생각하기 때문이다.</p>
<p>이해가 안 되지만 일단 저 가이드 문서를 따르라고 하니까 가이드 문서를 읽어봐야 했다. 이 <a href="https://packaging.python.org/">문서</a>에서는 <a href="https://packaging.python.org/tutorials/installing-packages/">패키지를 설치하는 방법</a>과 <a href="https://packaging.python.org/tutorials/distributing-packages/">패키징해서 배포하는 방법</a>을 설명하고 있는데 여기서는 후자가 필요하다고 생각해서 <a href="https://packaging.python.org/tutorials/distributing-packages/">패키징해서 배포하는 방법</a>을 살펴봤다.<br />
<br></p>
<h3>패키징과 배포의 요구사항</h3>
<p><a href="https://packaging.python.org/tutorials/installing-packages/#installing-requirements">패키지 설치 환경</a>을 구성하고 <code>pip install twine</code>로 <code>twine</code>을 설치한다. <code>twine</code>는 PyPI에 배포판을 업로드하는 데 필요한데 여기서는 PyPI에 배포할 것이 아니므로 일단 넘어갔다.<br />
<br></p>
<h3>프로젝트 구성</h3>
<p>여기서는 프로젝트 구성에 필요한 초기 파일에 관한 설명이 나와있다.</p>
<h4><code>setup.py</code></h4>
<p><code>setup.py</code>는 프로젝트 디렉터리의 루트에 존재해야 하는 가장 중요한 파일이다. 이 파일을 2가지 주요 기능이 있는데 첫째, 전역 <code>setup()</code> 함수를 가지고 있고 이 함수에 전달하는 인자는 프로젝트 정의에 대한 상세 내용을 담고 있고 둘째, 패키지 테스트와 관련된 여러 가지 명령어를 실행하는 CLI를 제공한다. 이 명령어의 목록을 보려면 <code>python setup.py --help-commands</code>를 실행하면 된다.</p>
<h4><code>setup.cfg</code></h4>
<p><code>setup.cfg</code>는 <code>setup.py</code> 명령어의 기본 옵션을 담고 있는 ini 파일이다.</p>
<h4><code>README.rst</code></h4>
<p><code>README.rst</code>는 프로젝트의 목적을 다루는 readme 파일이다. 가장 일반적인 형식이 <code>rst</code> 확장자를 가지는 reStructuredText이지만 필수는 아니다.</p>
<h4><code>MANIFEST.in</code></h4>
<p><code>MANIFEST.in</code>는 소스 배포판에 자동으로 포함되지 않는 추가 파일을 패키징 해야 할 때 필요하다. <code>MANIFEST.in</code>가 필요한 이유는 앞에서 살펴봐서 쉽게 이해하고 넘어갔다. 추가로 <code>MANIFEST.in</code>은 wheels처럼 바이너리 배포판에는 영향을 주지 않는다고 하는 걸 알게 되었다.</p>
<h4><code>LICENSE.txt</code></h4>
<p>모든 패키지는 배포판에 대한 라이센스 파일을 포함해야 한다.</p>
<h4><your package></h4>
<p>필수는 아니지만, 대부분은 프로젝트와 같은(혹은 아주 유사한) 이름의 최상위 패키지 아래 Python 모듈과 패키지를 포함하고 있다. 여기서 <code>flaskr</code>라는 프로젝트 에서 <code>flaskr</code>라는 폴더를 가지는 것을 의미하는 것이다.</p>
<h4>setup() 인자</h4>
<p><code>setup.py</code>의 주요 기능이 전역 <code>setup()</code> 함수를 가지는 것이므로 이 함수에 전달하는 인자로 프로젝트 정의에 대한 자세한 내용을 명시한다.</p>
<ul>
<li><code>name</code>: 프로젝트 이름으로 PyPi 목록에 표시되는 이름이다. <a href="https://www.python.org/dev/peps/pep-0508">PEP 508</a>에 따르면 유효한 프로젝트 이름은 ASCII 문자, 숫자, 언더스코어(<code>_</code>), 하이픈(<code>-</code>), 마침표(<code>.</code>)로만 이뤄져야 하고 ASCII 문자나 숫자로 시작하고 끝난다. <code>name='sample',</code>처럼 지정한다.<br />
<br />
<ul>
<li>프로젝트 이름을 비교할 때는 대소문자를 구별하지 않고 언더스코어, 하이픈, 마침표가 여러 개 이어지더라도 같은 것으로 처리하므로 프로젝트 이름이 <code>cool-stuff</code>일 때 사용자가 의존성 선언이나 다운로드를 할 때 <code>Cool-Stuff</code>, <code>cool.stuff</code>, <code>COOL_STUFF</code>, <code>CoOl__-.-__sTuFF</code>를 모두 같은 것으로 처리한다.</li>
</ul></li>
<li><code>version</code>: 프로젝트의 현재 버전으로 사용자가 최신버전 대신 특정 버전을 사용할 수 있도록 한다. <code>version='1.2.0',</code>처럼 지정한다.</li>
<li><code>url</code>: 프로젝트의 홈페이지 URL로 <code>url='https://github.com/pypa/sampleproject',</code> 처럼 지정한다.</li>
<li><code>author</code>: 프로젝트의 작성자 정보다. <code>author='The Python Packaging Authority',</code>나 <code>author_email='pypa-dev@googlegroups.com',</code> 처럼 지정한다.</li>
<li><code>license</code>: 사용하는 라이센스로 <code>license='MIT',</code>처럼 지정한다.</li>
<li><code>classifiers</code>: 프로젝트를 카테고리화 하는 <a href="https://pypi.python.org/pypi?%3Aaction=list_classifiers">classifier 목록</a>이다. 프로젝트가 지원하는 Python 버전을 정의하기도 하지만 이는 PyPI에서 검색하는 용도로만 사용해야 하고 설치에서는 사용하면 안 된다. 프로젝트 설치에 사용하는 파이썬 버전 제약은 <code>python_requires</code>를 사용해야 한다.</li>
</ul>
<pre class="line-numbers"><code class="language-python">classifiers=[
# How mature is this project? Common values are
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
'Development Status :: 3 - Alpha',
# Indicate who your project is intended for
'Intended Audience :: Developers',
'Topic :: Software Development :: Build Tools',
# Pick your license as you wish (should match "license" above)
'License :: OSI Approved :: MIT License',
# Specify the Python versions you support here. In particular, ensure
# that you indicate whether you support Python 2, Python 3 or both.
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
],
</code></pre>
<ul>
<li><code>keywords</code>: 프로젝트를 설명하는 키워드 목록으로 <code>keywords='sample setuptools development',</code>처럼 지정한다.</li>
<li><code>packages</code>: 프로젝트에 포함되어야 하는 패키지 목록이다. 수동으로 지정할 수도 있지만 <code>setuptools.find_packages</code>가 자동으로 찾아준다. <code>exclude</code> 키워드는 릴리스나 설치 시에 생략할 패키지를 지정할 때 사용한다. <code>packages=find_packages(exclude=['contrib', 'docs', 'tests*']),</code>처럼 지정한다.</li>
<li><code>install_requires</code>: 프로젝트를 실행할 때 최소한으로 필요한 의존성을 지정할 때 사용한다. <code>pip</code>로 설치했다면 의존성으로 설치하는 것이 명세이다. <code>install_requires=['peppercorn'],</code>처럼 지정한다.</li>
<li><code>python_requires</code>: 프로젝트를 특정 파이썬 버전에서만 실행해야 한다면 <a href="https://www.python.org/dev/peps/pep-0440">PEP 440</a> 버전 지정자 문자열로 pip가 다른 파이썬 버전에서는 설치하지 못하도록 할 수 있다. 이 기능은 최신 기능이므로 프로젝트의 소스 배포판과 wheels를 setupttols 24.2.0 이상으로 빌드해야 적절한 메타데이터가 생성되고 pip 9.0.0 이상에서만 이 메타데이터를 인식한다.<br />
<br />
<ul>
<li><code>python_requires='>=3',</code>는 Python 3+을 지원함을 의미한다.</li>
<li><code>python_requires='~=3.3',</code>는 Python 3.3 이상은 지원하지만, Python 4는 지원하지 않음을 의미한다.</li>
<li><code>python_requires='>=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4',</code>는 Python 2.6, 2.7과 3.3 이상의 모든 Python 3를 지원함을 의미한다.</li>
</ul></li>
<li><code>package_data</code>: 패키지에 설치되어야 하는 추가 파일이 있을 때 사용한다. 이 파일들은 주로 패키지 구현체와 관련된 데이터나 패키지를 사용하는 프로그래머가 관심 있을 문서를 포함한 텍스트 파일이다. 이러한 파일을 "package data"라고 부른다. 이 값은 패키지 이름에서 상대 경로의 목록으로 패키지에 복사되어야 한다. <code>package_data={'sample': ['package_data.dat'], },</code>처럼 지정한다.</li>
<li><code>data_files</code>: 대부분은 <code>package_data</code>로 충분하지만, 일부의 경우 패키지 외부의 데이터 파일을 두어야 하는 경우가 있다. <code>data_files</code>는 이때 사용한다. <code>data_files=[('my_data', ['data/data_file'])],</code>처럼 지정한다.</li>
<li><code>scripts</code>: 설치해야 하는 미리 만들어 놓은 스크립트를 지정할 때 사용하지만 크로스 플랫폼 호환성을 위해 <code>console_scripts</code>를 사용하기를 권장한다.</li>
<li><code>py_modules</code>: 프로젝트에 포함되어야 하는 모듈의 목록으로 <code>py_modules=["six"],</code>처럼 지정한다.</li>
<li><code>entry_points</code>: 프로젝트 등에서 의존해서 정의하고 있는 엔트리포인트를 위해 프로젝트가 제공하는 플러그인을 지정한다. <code>entry_points={ ... },</code>처럼 지정한다.</li>
<li><code>console_scripts</code>: 스크립트 인터페이스를 등록할 때 사용한다. 다음과 같이 지정할 수 있다.</li>
</ul>
<pre class="line-numbers"><code class="language-python">entry_points={
'console_scripts': [
'sample=sample:main',
],
},
</code></pre>
<p>여기까지 문서를 읽었지만 주로 패키지를 설정하는 내용이고 <a href="https://packaging.python.org/tutorials/distributing-packages/#working-in-development-mode">"개발 모드"로 작업하기</a>를 보면 필수는 아닌데 작업하는 동안 프로젝트를 "editable"이나 "develop" 모드로 로컬에 설치하는 게 일반적이라고 한다. 이렇게 하면 프로젝트를 설치한 상태로 수정하면서 작업할 수 있다. 이 모드는 <code>pip install -e .</code>를 실행하면 되고 <code>-e</code> 대신 <code>--editable</code>을 사용해도 된다. 이렇게 실행했을 때 <code>install_requires</code>에 선언한 의존성과 <code>console_scripts</code>에 선언한 스크립트를 모두 설치한다.</p>
<p>사실 여전히 왜 모듈도 아니고 웹 애플리케이션 같은 걸 작성할 때도 패키지를 설치하듯이 하는지 잘 모르겠지만 Python의 역사 때문에 남은 관례가 아닌가 추측만 해본다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1343?commentInput=true#entry1343WriteComment">댓글 쓰기</a></strong></p>Flask 공식 튜토리얼 따라하기 #1Outsiderhttps://blog.outsider.ne.kr/13292017-11-12T22:32:27+09:002017-11-12T22:32:27+09:00<p><a href="https://blog.outsider.ne.kr/1325">Flask 개발환경을 구성</a>했으므로 이제 Flask 애플리케이션을 개발할 차례가 되었다. 문서를 보면서 대충 눈치껏 만들어 보려고 했지만, 막상 만들려고 하니까 모르는 부분이 너무 많았다.</p>
<ul>
<li>프로젝트 폴더/파일 구조는 어떻게 가져가야 하는가?</li>
<li>웹 애플리케이션의 진입점이 되는 라우팅 처리와 HTTP 메서드 처리는 어떻게 하는가?</li>
<li>뷰 템플릿인 <a href="http://jinja.pocoo.org/">Jinja2</a>는 어떻게 사용해서 페이지를 제공할 수 있는가?</li>
<li>HTML을 반환할 때와 JSON을 응답할 때는 어떻게 처리해야 하는가?(content negotiation?)</li>
<li>DB 연동을 어떻게 해야 하는가?(추가로 SQL Alchemy?)</li>
<li>정적 파일은 어떻게 제공하는가?</li>
<li>Flask를 설치할 때 <code>Werkzeug</code>, <code>itsdangerous</code>, <code>click</code> 패키지를 함께 설치하는데 어떤 패키지일까?</li>
<li>테스트는 어떻게 작성하는가?</li>
</ul>
<p><a href="https://github.com/pallets/flask/tree/master/examples/flaskr">예제 코드</a>를 보고 해보다가 안 되겠다는 느낌이 들어서 <a href="https://blog.outsider.ne.kr/1324">Python 환경에 대해서 궁금한 걸 하나하나 정리</a>하고 나니 이해하기가 한결 편해져서 Flask도 <a href="http://flask.pocoo.org/docs/0.12/tutorial/introduction/">튜토리얼</a>부터 차근차근히 해보기로 했다. 중간중간 자꾸 대충 이해할 것 같은 기분에 건너뛰고 싶은 충동이 느껴져서 이번에도 모르는 부분을 다 찾아보면서 일단 Flask 애플리케이션 개발 및 Python 개발에 대해서 좀 더 이해해 보기로 했다.</p>
<p><a href="http://flask.pocoo.org/docs/0.12/tutorial/introduction/">튜토리얼</a>은 Flask로 간단한 블로그 애플리케이션 <a href="https://github.com/pallets/flask/tree/master/examples/flaskr">flaskr</a>을 만드는 과정을 설명한다. 이 블로그는 설정해 놓은 사용자로 로그인을 할 수 있고 사용자는 제목과 내용(HTML)으로 글을 올릴 수 있고 첫 페이지에서 올려진 글 목록을 보여준다.<br />
<br></p>
<h1><a href="http://flask.pocoo.org/docs/0.12/tutorial/folders/#tutorial-folders">0 단계: 폴더 생성</a></h1>
<pre class="line-numbers"><code class="language-clike">└── flaskr/
└── flaskr/
├── static/
└── templates/
</code></pre>
<p>튜토리얼에서는 위와 같은 폴더 구조를 제안하고 있다. 이게 Flask의 관례인지 Python의 관례인지는 아직 잘 모르겠지만 프로젝트명으로 된 루트 디렉터리 아래 같은 이름에 디렉터리가 또 있다는 부분 그러니까 <code>flaskr/flaskr</code>의 형태가 좀 어색하게 느껴졌다. <code>src</code>같은 게 더 자연스럽지 않나 생각하는데 느낌상 배포 등을 할 때 이름을 지정하는 것과 관련이 있지 않을까 상상 정도를 해본다. 그러고 보니 회사에서 보던 Django 프로젝트도 저런 형태의 폴더구조를 본 기억이 났다. <code>FLASK_APP=flaskr.factory:create_app()</code> 처럼 환경변수를 지정할 때 <code>FLASK_APP=src.factory:create_app()</code> 같은 형태가 되면 이상하니까 그런 게 아닐까 정도의 생각이다.</p>
<p>여기서 <code>static/</code> 폴더는 <code>.js</code>나 <code>.css</code> 같은 정적 파일을 모아두는 폴더고 <code>templates/</code>는 <a href="http://jinja.pocoo.org/">Jinja2</a>의 뷰 파일을 두는 폴더다.<br />
<br></p>
<h1><a href="http://flask.pocoo.org/docs/0.12/tutorial/schema/#tutorial-schema">1 단계: 데이터베이스 스키마</a></h1>
<pre class="line-numbers"><code class="language-sql">drop table if exists entries;
create table entries (
id integer primary key autoincrement,
title text not null,
'text' text not null
);
</code></pre>
<p>위 내용으로 <code>flaskr/flaskr/schema.sql</code> 파일을 생성한다. 블로그에서 사용할 간단한 테이블이다. <a href="http://alembic.zzzcomputing.com/en/latest/index.html">Alembic</a>같은 마이그레이션 도구를 사용하는 것은 아니지만 <code>drop</code> 테이블이 있는 것으로 보아 마이그레이션 도구와 비슷하게 사용하려는 것으로 보인다. 참고로 이 튜토리얼에서는 <a href="https://www.sqlite.org/">SQLite</a>를 쓰는데 나는 SQLite로 애플리케이션을 만드는 경우가 거의 없어서 크게 관심 없지만 일단 튜토리얼이라서 그대로 따라 해보고 있다.<br />
<br></p>
<h1><a href="http://flask.pocoo.org/docs/0.12/tutorial/setup/#tutorial-setup">2 단계: 애플리케이션 설정 코드</a></h1>
<p>여기서는 애플리케이션을 설정하는 코드를 작성한다. <code>flaskr/flaskr</code> 아래 <code>flaskr.py</code> 파일을 다음의 내용으로 만든다.</p>
<pre class="line-numbers"><code class="language-python">import os
import sqlite3
from flask import Flask, request, session, g, redirect, url_for, abort, \
render_template, flash
app = Flask(__name__) # create the application instance :)
app.config.from_object(__name__) # load config from this file , flaskr.py
# Load default config and override config from an environment variable
app.config.update(dict(
DATABASE=os.path.join(app.root_path, 'flaskr.db'),
SECRET_KEY='development key',
USERNAME='admin',
PASSWORD='default'
))
app.config.from_envvar('FLASKR_SETTINGS', silent=True)
</code></pre>
<p><code>import</code> 다음에는 2줄을 띄우는 게 관례인 것 같다. <a href="https://www.python.org/dev/peps/pep-0008/">PEP 8</a>에는 최상위 함수와 클래스 정의는 2줄을 띄운다고 나와 있는데 최상위 함수의 기분이 좀 헷갈리지만 일단 <code>import</code> 구문과 코드를 구분하는데 2줄을 띄우는 정도는 이해했다. 난 2줄을 띄우는 습관이 없어서 아직 엄청 어색하지만... 관례가 익숙지 않으므로 <a href="https://pypi.python.org/pypi/flake8">flake8</a> 같은 도구를 사용해서 작성한 코드를 계속 검색할 수 있게 해야할텐데 이 방법은 이후 더 찾아봐야 할 것 같다.</p>
<p><code>app = Flask(__name__)</code>는 주석 설명대로 Flask 인스턴스를 생성한 것이다. python에서 <a href="https://docs.python.org/3/reference/import.html?highlight=__name__#__name__"><code>__name__</code></a>은 모듈의 이름을 뜻한다. 모듈의 이름이란 게 애매한 의미로 느껴지는 데 테스트해보면 파일명이 바로 모듈의 이름이 되는 것으로 보인다. 그래서 이 튜토리얼은 <code>flaskr</code>를 만들기 때문에 시작 파일이 <code>flaskr.py</code>가 되는 것 같다. 여기서 모듈 이름이란 의미는 <code>flaskr.py</code>를 <code>import</code>해서 사용하면 <code>__name__</code>의 값이 <code>flaskr</code>이 된다. 추가로 <code>python flaskr.py</code>처럼 직접 실행할 때는 <code>__name__</code>의 값이 <code>__main__</code>가 된다. 파이썬 코드에서 많이 본 <code>if __name__ == "__main__":</code>는 <code>import</code>해서 사용하는 경우인지 직접 실행한 경우인지를 구분하기 위한 것이다.</p>
<p>그다음은 <code>app.config.update()</code>에 딕션어리를 넣고 있다. 코드를 보면 데이터베이스 관련 설정값을 추가하는 것으로 충분히 예측할 수 있다. <code>app.conf</code>를 출력해 보면 다음과 같이 출력된다.</p>
<pre class="line-numbers"><code class="language-clike"><Config {
'DEBUG': False,
'TESTING': False,
'PROPAGATE_EXCEPTIONS': None,
'PRESERVE_CONTEXT_ON_EXCEPTION': None,
'SECRET_KEY': None,
'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31),
'USE_X_SENDFILE': False,
'LOGGER_NAME': 'flaskr',
'LOGGER_HANDLER_POLICY': 'always',
'SERVER_NAME': None,
'APPLICATION_ROOT': None,
'SESSION_COOKIE_NAME': 'session',
'SESSION_COOKIE_DOMAIN': None,
'SESSION_COOKIE_PATH': None,
'SESSION_COOKIE_HTTPONLY': True,
'SESSION_COOKIE_SECURE': False,
'SESSION_REFRESH_EACH_REQUEST': True,
'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0, 43200),
'TRAP_BAD_REQUEST_ERRORS': False,
'TRAP_HTTP_EXCEPTIONS': False,
'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http',
'JSON_AS_ASCII': True,
'JSON_SORT_KEYS': True,
'JSONIFY_PRETTYPRINT_REGULAR': True,
'JSONIFY_MIMETYPE': 'application/json',
'TEMPLATES_AUTO_RELOAD': None}>
</code></pre>
<p>지금 당장은 각 값의 사용 용도는 알 수 없지만 대충 예상할 수는 있다. 앞의 코드처럼 딕션어리로 업데이트한 뒤의 값을 보면 <code>SECRET_KEY</code>처럼 이미 있던 키는 갱신되고 새로운 키는 추가되는 것을 확인할 수 있다.</p>
<p>이 <code>app.config</code>를 업데이트하는 값을 보면 <code>DATABASE=os.path.join(app.root_path, 'flaskr.db')</code>부분이 있다. <a href="https://docs.python.org/3/library/os.path.html">os.path</a>는 경로와 관련된 모듈이다. <a href="https://docs.python.org/3/library/os.path.html#os.path.join">os.path.join()</a>은 전달한 값을 연결한 경로로 반환한다. <code>app.root_path</code>는 <a href="http://flask.pocoo.org/docs/0.12/api/#application-object">Flask 애플리케이션 객체</a>에 있는 속성인데 문서를 보면 자동으로 애플리케이션의 루트 경로로 잡힌다. 그래서 <code>flaskr/flaskr/flaskr.py</code> 파일이 있는 <code>/Users/outsider/flaskr/flaskr/</code>가 이 값이 되고 <code>flaskr.db</code>와 연결했으므로 최종적으로 <code>/Users/outsider/flaskr/flaskr/flaskr.db</code>가 된다.<br />
<br></p>
<h2>인스턴스 폴더</h2>
<p>튜토리얼의 설명을 보면 OS가 프로세스의 워킹 디렉터리를 할 수 있지만 보통 한 프로세스에서 여러 애플리케이션을 띄울 수 있으므로 <code>app.root_path</code>를 사용한다고 나와 있고 데모가 아닌 실제 애플리케이션에서는 <a href="http://flask.pocoo.org/docs/0.12/config/#instance-folders">Instance Folder</a>를 추천한다고 나와 있다. 그래서 인스턴스 폴더가 궁금해졌다.</p>
<p>인스턴스 폴더는 Flask 애플리케이션을 생성할 때 <code>app = Flask(__name__, instance_path='/path/to/instance/folder')</code>처럼 지정할 수 있다. 실제로 어떤 상황에 유용한지는 Flask를 더 써봐야겠지만 <a href="http://flask.pocoo.org/docs/0.12/config/#instance-folders">문서</a>를 보면 <code>app.root_path</code>는 패키징되지 않은 경우에만 잘 동작한다고 되어 있다. 모듈로 PyPI에 올리는 경우가 아닌 Flask처럼 웹 애플리케이션을 작성하는 상황에서 Python에서 말하는 패키징이라는 개념을 아직 제대로 이해하지 못했지만, Flask 애플리케이션을 다른 곳에서 불러다 사용하는 경우 <code>root_path</code>는 패키지의 루트 경로가 될 것이므로 불러올 구성 파일 같은 경우도 패키지에 포함되어야 한다. 이때 <code>instance_path</code>를 다른 경로의 파일을 참조하도록 할 수 있고 패키지에 포함하지 않아도 되므로 시크릿 정보가 있어서 설정 파일을 다른 곳에 두거나 외부에서 구성파일을 받고자 하는 경우 사용하는 것으로 이해했다. 아니면 서버 배포를 하는 경우도 스테이지나 프로덕션 서버별로 파일을 다르게 지정해서 배포할 수 있으니 이때도 사용할 수 있다.</p>
<p>지정하지 않으면 루트 경로의 <code>/instance</code>가 값이 되고 설치한 모듈이나 패키지일 때 <code>/path/to/site-packages/myapp</code> 등이 된다. 지정할 때는 반드시 절대 경로로 지정해야 하고 상대경로를 사용해야 하는 경우는 <code>app = Flask(__name__, instance_relative_config=True)</code>처럼 설정해 주어야 한다.<br />
<br></p>
<h2>설정 파일 분리</h2>
<p>튜토리얼에서는 <code>flaskr.py</code> 파일에 직접 설정하고 있지만, 보통은 별도의 <code>.ini</code>나 <code>.py</code> 파일로 분리한 뒤 가져와서 사용하는 것을 추천한다. 보통 이 파일은 외부에서 제어할 수 있는 게 좋으므로 튜토리얼에서도 환경변수에서 가져오는 방법도 설명하고 있다. <code>app.config.from_envvar('FLASKR_SETTINGS', silent=True)</code>를 사용하면 <code>FLASKR_SETTINGS</code>라는 환경변수의 경로에 있는 파일에서 값을 가져와서 <code>Config</code> 객체에 설정할 수 있다. 여기서 파일의 종류가 문서에 명확하게 나와 있지 않아서 헷갈리는데 찾아보니 보통은 <code>cfg</code> 파일을 사용하는 것 같다.</p>
<p>그래서 위와 똑같은 설정을 하려면 아래와 같이 <code>config.cfg</code>를 만들고</p>
<pre class="line-numbers"><code class="language-clike">DATABASE='/Users/outsider/flaskr/flaskr/flaskr.db'
SECRET_KEY='development key'
USERNAME='admin'
PASSWORD='default'
</code></pre>
<p><code>FLASKR_SETTINGS=/Users/outsider/flaskr/config.cfg</code> 환경 변수를 지정하면 <code>Config</code> 객체에 위의 내용이 들어가게 된다. 이때 <code>silent=True</code>를 하면 해당 환경변수가 없어도 오류가 나지 않는다.</p>
<p>JSON 파일을 사용하고 싶다면 <code>app.config.from_json(os.environ['FLASKR_SETTINGS'], silent=True)</code>처럼 사용할 수 있다. 이 외에도 파이썬 객체에서 가져오는 <code>from_object</code> 등 여러 가지가 있어서 필요 한대로 설정 파일을 가져와서 사용할 수 있다.<br />
<br></p>
<h2>데이터베이스 접속</h2>
<p>이번 튜토리얼의 마지막으로 <code>flaskr/flaskr/flaskr.py</code> 파일에 데이터베이스(여기서는 SQLite3) 접속에 사용한 함수를 추가한다.</p>
<pre class="line-numbers"><code class="language-python">def connect_db():
"""Connects to the specific database."""
rv = sqlite3.connect(app.config['DATABASE'])
rv.row_factory = sqlite3.Row
return rv
</code></pre>
<p>튜토리얼에 아직 이 부분을 사용하는 코드가 나오지 않아서 정확한 위치를 모르겠는데 그냥 <code>flaskr.py</code>에 추가했다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1329?commentInput=true#entry1329WriteComment">댓글 쓰기</a></strong></p>Flask 애플리케이션 개발 환경 구성Outsiderhttps://blog.outsider.ne.kr/13252017-10-09T15:05:15+09:002017-10-09T01:45:57+09:00<p><a href="https://blog.outsider.ne.kr/1324">Python 개발 환경을 구성</a>했으니 이제 프로젝트를 시작해야 한다. 나는 주로 웹 쪽을 하므로 웹 프레임워크를 선택해야 했고 대표적으로 <a href="https://www.djangoproject.com/">django</a>와 <a href="http://flask.pocoo.org/">Flask</a>가 있는데 개인적으로 Flask 쪽에 더 맘에 가긴 했지만, 더 좋은 프레임워크도 있을 수 있으므로 <a href="https://www.facebook.com/jeonghoon.byun/posts/10155190063778472">Facebook에 질문을 올렸다</a>. 많은 분이 의견을 주셨지만 <a href="https://twitter.com/hongminhee">홍민희</a>님의 대답이 맘에 들어서 여기에 남긴다.</p>
<blockquote>
<p>파이썬 웹 프로그래밍을 처음 시작하신다는 배경 하에, Flask의 큰 장점은 파일 하나만 보는 것으로 시작할 수 있다는 점입니다. 예를 들어 Django 등의 풀스택 웹 프레임워크는 일반적으로 킥스타터의 도움을 받아 초기 파일들을 생성해주는 방식으로 시작하게 되는데, 생성된 파일이 적지 않으므로 처음 시작할 때 부담이 될 수 있습니다. 물론 이 부분은 개인차가 있을 수 있습니다. 만들어진 파일 하나 하나가 어떤 역할을 하는지 알지 않으면 신경이 쓰이는 저 같은 경우에는 Flask처럼 파일 하나로 시작하고, 그 파일조차 제가 직접 에디터에 입력하는 것으로 출발할 수 있는 쪽이 오롯이 지금 배우기 시작하는 분야에 집중하기 좋았습니다. 반면 딱히 생성되는 파일들이 어떤 역할을 하는지 이해하지 않더라도, 지금은 쓰지 않더라도 나중에 쓸 날이 오겠지 생각하고 넘어갈 수 있다면 Flask의 이런 점은 별 장점은 아닐 수 있습니다.</p>
</blockquote>
<p>회사에서는 주로 django를 쓰고 있어서 django를 배워야 하긴 하지만 일단 그 전에 파이썬에 대해서 이해할 게 더 많다고 느껴졌다. 여태 개발해오면서도 Rails나 django 식의 올인원(?) 프레임워크를 안 써봐서 그런지 왠지 모를 거부감이 있었다. 사용하다 보면 결국 내부를 자세히 이해해야 잘 쓸 수 있겠지만 위에 홍민희 님이 얘기하신 것처럼 추상화 레벨이 너무 높아서 감춰진 영역이 너무 많게 느껴졌다. 빨리 웹사이트를 만들고 어드민 기능까지 바로 되어서 편할 수도 있지만, 나한테는 알 수 없이 동작하는 블랙박스처럼 느껴졌다. 그러고 보면 그동안 내가 관심 가졌던 웹 프레임워크 사이트에 들어가면 주로 "<a href="http://www.sinatrarb.com/">Sinatra</a> inspired"라고 쓰여 있긴 했었다. 정작 Sinatra는 한 번도 안 써봤지만...</p>
<p>그리고 너무 갖추어진 환경에서 개발하는 걸 별로 안 좋아하는 게 해당 기능이 어디서 제공하는 건지 알 수 없다는 부분도 있다. 예를 들면 A 기능을 django에서 제공한다고 했을 때 이 A 기능이 원래 Python에서도 보통 사용하는 기능인데 django가 감싸거나 확장해서 제공하는 것인지 아니면 다른 Python 프로젝트에서는 없는 기능인데 django만의 기능인지를 처음 사용하는 처지에서는 구분하기 어려워진다. 각 파일의 용도나 관례를 좀 파악한 다음에 추상화된 계층을 보는 게 더 이해하기 쉽다고 생각했다.<br />
<br></p>
<h1>Flask 프로젝트 구성</h1>
<p>그래서 Flask를 사용하기로 하고 구성하기로 했다. 일단 <a href="http://flask.pocoo.org/">Flask 홈페이지</a>에 나와 있는 대로 <code>pip install flask</code>로 <code>flask</code>를 설치했다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<p>Flask를 설치하자 <code>MarkupSafe</code>, <code>Jinja2</code>, <code>click</code>, <code>itsdangerous</code>, <code>Werkzeug</code>, <code>flask</code>가 모두 설치되었다. 어디서 의존성이 같이 내려왔는지 궁금하지만 일단은 홈페이지에 나온 대로 진행을 해보자. 다음의 내용으로 <code>hello.py</code> 파일을 만들었다.</p>
<pre class="line-numbers"><code class="language-python"># hello.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
</code></pre>
<p>이제 <code>FLASK_APP=hello.py flask run</code>으로 실행을 하자 <code>Hello World</code> 웹사이트가 잘 떴다.</p>
<pre class="line-numbers"><code class="language-bash">$ FLASK_APP=hello.py flask run
* Serving Flask app "hello"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
</code></pre>
<p>여기까지만 했는데도 코드와 관련 없이 궁금한 게 많이 생겼다.</p>
<ul>
<li><code>pip install flask</code>만 하니 관련 의존성이 다 설치되었는데 이 의존성은 어떻게 관리되는가?</li>
<li><code>FLASK_APP</code> 환경변수의 의미는 무엇일까?</li>
<li><code>flask</code> 애플리케이션을 실행했더니 현재 위치에 <code>__pycache__</code> 폴더가 생겼는데 이 폴더의 용도는 무엇일까?</li>
<li>이 프로젝트가 <code>flask</code>를 사용하므로 소스코드를 다운받은 후 <code>flask</code>를 설치하도록 의존성 관리를 해야 하는데 이는 어떻게 하는가?</li>
</ul>
<p>Python에 대해서 아는 게 참 없구나 싶은 기분이었다. 다른 사람도 Node.js 처음 하면 이렇게 궁금한 게 많아지는가 싶기도 하면서...<br />
<br></p>
<h2>flask의 의존성 관리</h2>
<p><a href="https://github.com/pallets/flask">flask 저장소</a>로 가봤다. 최근에 본 <a href="https://blog.outsider.ne.kr/1296">파이썬 3에 뛰어들기</a>에서 <code>setup.py</code>로 패키징하는 내용이 있어서 <a href="https://github.com/pallets/flask/blob/master/setup.py">setup.py</a>를 열어보니 다음과 같은 부분이 있었다.</p>
<pre class="line-numbers"><code class="language-python">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'
],
}
)
</code></pre>
<p>이 파일이 Node.js의 <code>package.json</code>과 같은 역할을 하는 것으로 보였다. 프로젝트에 대한 설명이 있고 <code>install_requires</code>에 있는 의존성이 앞에서 설치된 패키지와 같은 것으로 보아 이 파일을 참고해서 설치한 것으로 보인다. <code>extras_require</code>는 <code>flask</code>를 가져다 쓸 때는 필요 없지만 개발할 때만 필요한 개발 의존성으로 보인다.</p>
<p><code>virtualenv</code>를 생성하고 설치했으므로 설치된 위치를 확인해 보니 현재 프로젝트의 <code>./venv/lib/python3.6/site-packages/</code> 아래에 다음과 같이 설치되어 있다. <code>venv</code>가 <code>virtualenv</code>의 가상환경 이름이다.</p>
<pre class="line-numbers"><code class="language-bash">├── 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
</code></pre>
<p><code>setup.py</code>가 Python 코드라서 용도를 잘 모르겠는 코드가 있는 것은 둘째치고라도 <code>[파이썬 3에 뛰어들기](https://blog.outsider.ne.kr/1296)</code>에서는 상단에서 <code>from distutils.core import setup</code>로 쓰고 있었는데 <code>flask</code>의 코드를 보면 <code>from setuptools import setup</code>로 되어 있다. 눈치껏 <code>setup</code>은 같은 역할을 하는 것 같은데 <code>distutils</code>과 <code>setuptools</code>이 뭐가 다르고 왜 다르게 사용하는지 모르겠다. 궁금한 게 계속 나와서 진도를 못 나가...</p>
<p><a href="https://stackoverflow.com/a/14753678/518864">Stackoverflow에서 잘 정리된 답변</a>을 발견했다.</p>
<ul>
<li><a href="https://docs.python.org/3/library/distutils.html">Distutils</a>: 아직 Python 패키징의 표준 도구로 Python 2부터 3까지 표준 라이브러리로 포함되어 있다.</li>
<li><a href="https://setuptools.readthedocs.io/en/latest/">Setuptools</a>: Distutils의 부족한 부분을 채우려고 개발된 도구로 표준 라이브러리에 포함되어 있지 않다. 여기서 <code>easy_install</code>이 도입되었다.</li>
</ul>
<p>패키징해서 배포를 안 해봐서 아직 감이 다 오진 않지만 <a href="https://pypi.org/">PyPI</a>에 패키지를 올릴 때 사용하는 도구로 대충 이해했다. 둘 다 Python 패키지를 설치하는 도구인 <a href="https://pip.pypa.io/en/stable/">pip</a>와 잘 동작한다고 한다. 저 글의 작성자는 <code>Setuptools</code>가 <code>virtualenv</code>와 <code>pip</code>에서 아주 잘 동작하므로 권장하고 있었다. 나중에 쓸 때 <code>Setuptools</code>를 자세히 찾아봐서 쓰면 되겠다 싶다.</p>
<p>관련해서 찾다 보니 몇 가지 더 궁금한 단어들이 생겼다.</p>
<ul>
<li><a href="https://wheel.readthedocs.io/en/latest/">wheel</a>: Python의 패키지 형식으로 <code>.whl</code> 확장자를 가지고 ZIP 형식이다. <code>virtualenv</code> 생성하면 <code>wheel</code> 커맨드라인 명령어도 생기던데 아직 용도는 잘 모르겠다.</li>
<li><a href="http://peak.telecommunity.com/DevCenter/EasyInstall">easy_install</a>** : Setuptools에 포함된 도구라고 한다. 이게 궁금했던 건 <code>virtualenv</code>를 생성하면 <code>easy_install</code> 커맨드라인 명령어가 생겨서였는데 이전에는 무심코 넘어갔는데 <code>virtualenv</code>를 생성할 때 <code>Installing setuptools, pip, wheel...done.</code>라는 문구가 나오는 걸 깨닫게 되었다. <a href="https://virtualenv.pypa.io/en/stable/">virtualenv</a>에서 이 설치 과정까지 포함된 것으로 보인다. <a href="https://packaging.python.org/discussions/pip-vs-easy-install/">pip vs easy_install</a>를 보면 easy_install은 2004년에 setuptools의 일부로 릴리스 되었고 pip는 2008년에 easy_install을 대체하려고 나왔다. 그리고 Python 프로젝트 볼 때 나한테는 어색하게 느껴졌던 <a href="https://pip.pypa.io/en/latest/user_guide/#requirements-files">requirements.txt</a>가 pip에서 도입되었다고 한다.</li>
<li><a href="http://peak.telecommunity.com/DevCenter/PythonEggs">Eggs</a>: <a href="https://packaging.python.org/discussions/pip-vs-easy-install/">pip vs easy_install</a>을 보다 보니 <code>Eggs</code>란 용어가 나왔다. <code>setuptools</code>에서 도입된 빌드된 배포형식인데 <code>Eggs are to Pythons as Jars are to Java..</code>라는 설명을 보니 감이 왔다. <a href="https://packaging.python.org/discussions/pip-vs-easy-install/">pip vs easy_install</a> 문서의 비교표를 보면 <code>pip</code>는 <code>wheel</code>을 쓰고 <code>easy_install</code>은 <code>Eggs</code>를 쓰는 것 같다. 여기서 <a href="https://packaging.python.org/glossary/#term-built-distribution">built distribution</a>이라는 용어가 나오는데 설명을 보면 설치할 시스템에 복사만 하면 되는 배포 형식이라고 하는데 이 설명을 보면 <code>wheel</code>은 설치 전에 빌드 과정이 필요하므로 컴파일되지 않은 Python 파일이 담겨 있고 반대로 <code>Eggs</code>는 컴파일된 결과만 포함하고 있는 것이 둘의 가장 큰 차이로 생각된다.</li>
</ul>
<p>예전에 Python의 패키지 관리 도구의 험난한(?) 역사에 대한 글을 본 기억이 있는데 그 긴 역사를 거치면서 남은 잔재가 아닌가 싶다.<br />
<br></p>
<h2><code>FLASK_APP</code></h2>
<p>Flask 홈페이지에서 <code>FLASK_APP=hello.py flask run</code>로 실행하도록 안내하고 있는데 <a href="http://flask.pocoo.org/docs/0.12/quickstart/">Quickstart</a>를 보면 <code>app = Flask(__name__)</code>에서 애플리케이션의 모듈이나 패키지 이름으로 <code>Flask</code> 인스턴스를 생성하고 있다. 여기서 <code>__name__</code>은 현재 모듈의 이름을 담고 있는 Python의 내장 변수이다.</p>
<p><a href="https://github.com/pallets/flask/blob/851eaa4db7cef513dae35286d816867d68a72049/docs/cli.rst#basic-usage">cli 사용법</a>을 보면 <code>FLASK_APP</code> 환경변수를 <code>flask</code> 커맨드라인 명령어가 애플리케이션을 찾을 수 있게 하는 것으로 보인다. 이 이름을 통해 템플릿이나 정적 파일을 어디서 찾아야 하는지 찾는다고 한다. 자세한 건 Flask 앱을 개발해봐야 더 이해할 수 있을 것 같다.<br />
<br></p>
<h1><code>__pycache__</code></h1>
<p>인터넷에서 찾아보니 Python 3.2부터 도입되었는데 컴파일된 바이트 코드가 들어간다고 한다. Python 2.x의 경험이 별로 없어서 몰랐지만 Python 2.x에서는 <code>hello.py</code>가 있으면 그 옆에 <code>hello.pyc</code> 바이트 코드 파일이 생기지만 3.2부터는 이 파일을 <code>__pycache__</code> 아래에 모은다고 한다.</p>
<p>이 관련 내용이 <a href="https://www.python.org/dev/peps/pep-3147/">PEP 3147 -- PYC Repository Directories</a>인데 이 문서를 읽어보면 성능향상을 위해서 Python 인터프리터가 컴파일한 바이트 코드를 파일시스템에 저장(<code>.pyc</code>)하고 이후에는 컴파일 단계를 건너뛰어서 더 빨리 모듈을 로딩할 수 있게 한다. CPython만 사용해 봤지만 여러 버전의 Python 인터프리터를 사용하는 경우 인터프리터 간에 <code>.pyc</code>를 공유할 수 없으므로 <code>__pycache__</code>를 만들어서 여러 인터프리터의 바이트 코드 파일을 같이 만들어 둘 수 있도록 하기 위함으로 보인다.</p>
<p>그래서 바이트 코드 파일에 매직 태그가 붙는데 CPython v3.6은 <code>.cpython-36.pyc</code>가 된다. 실제로 위 <code>hello.py</code> 파일을 실행한 결과는 <code>hello.cpython-36.pyc</code>로 <code>__pycache__</code>안에 생긴 것을 확인할 수 있다. 당연히 이 폴더는 VCS에서 관리할 필요가 없으므로 <code>.gitignore</code>등에서 제외해야 한다.<br />
<br></p>
<h1>프로젝트의 의존성 관리</h1>
<p>프로젝트를 개발하면서 필요한 의존성(현재까지는 <code>flask</code>)을 <code>pip</code>로 설치해서 사용하면 되는데 다른 사람이 보면 어떤 의존성이 필요한지 알 수 없으므로 프로젝트에 이 정보를 넣어야 한다. 앞에서 <code>flask</code>를 설치할 때 자동으로 <code>jinja2</code> 등이 설치된 것처럼 패키지로 배포했을 때도 이 정보가 필요하고 GitHub 등에 소스를 공개해두었을 때도 다른 사람이 다운받아서 실행하려면 프로젝트 내에 의존성이 명시되어있어야 하고 어떤 약속된 명령어를 실행했을 때 자동으로 관련 의존성을 모두 설치할 수 있어야 한다.</p>
<p>아직 Python에서 이 의존성을 어떻게 관리하는 알지 못하지만, 처음부터 이런 관리에 대해 구성을 해놓고 사용하고 싶었다. Flask 저장소에서 <a href="https://github.com/pallets/flask/tree/master/examples">예제</a>를 제공하고 있어서 일단 여기를 참고했다. 코드를 열어보니 맨 위에 <code># -*- coding: utf-8 -*-</code>가 있어서 Python 2.x에 맞춰진 것 같아서 좀 불안했지만, 의존성 관리가 달라졌을 것 같진 않아서 그냥 참고했다.</p>
<p>마이크로 블로그 예제인 <a href="https://github.com/pallets/flask/tree/master/examples/flaskr">flaskr</a>을 보니 소스코드는 <code>flaskr/</code>디렉터리에 넣고 테스트코드는 <code>tests/</code> 폴더 아래 둔다는 것을 알았다. <code>flaskr</code> 프로젝트 아래 <code>src</code>같은 폴더가 아니라 <code>flaskr</code>이라는 프로젝트 이름의 폴더를 한 번 더 만드는 게 이상했지만(<code>flaskr/flaskr</code>처럼 되니까...) 일단 그 전에 의존성을 먼저 해결해야 했기에 넘어갔다.</p>
<p>먼저 눈에 띈 파일은 <a href="https://github.com/pallets/flask/blob/master/examples/flaskr/setup.py">setup.py</a>와 <a href="https://github.com/pallets/flask/blob/master/examples/flaskr/setup.cfg">setup.cfg</a>였다.</p>
<h2>setup.py</h2>
<p>앞에서 Flask의 <code>setup.py</code>를 본대로 이 파일에 필요한 의존성이 명시되어 있다. 예제로 참고하는 <a href="https://github.com/pallets/flask/tree/master/examples/flaskr">flaskr</a>의 <code>setup.py</code>는 다음과 같이 되어 있다.</p>
<pre class="line-numbers"><code class="language-python"># -*- 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',
],
)
</code></pre>
<p>앞에서도 <code>setup.py</code>로 의존성을 관리하는 부분을 살펴봤는데 좀 더 이해할 필요가 있어 보였다. 이 파일이 <code>Distutils</code>부터 <code>Setuptools</code>까지 같이 쓰고 있는 것 같은데 <code>setup.py</code>에 대한 기본적인 내용은 <a href="https://docs.python.org/3/distutils/setupscript.html">https://docs.python.org/3/distutils/setupscript.html</a>에 나와 있다.</p>
<ul>
<li><code>name</code>은 패키지 이름이다.</li>
<li><code>packages</code>는 여기서 지적한 모듈을 <code>Distutils</code>가 찾아서 처리하도록 한다고 한다. 그래서 <code>packages = ['foo']</code>로 지정하면 <code>foo/__init__.py</code>를 찾는다.</li>
</ul>
<p>아래 키워드들은 <a href="https://setuptools.readthedocs.io/en/latest/">Setuptools</a>에서 추가하거나 확장한 키워드였다.</p>
<ul>
<li><code>find_packages()</code>는 <code>packages</code>를 수동으로 지정하는 게 큰 프로젝트에서는 어려우므로 이 함수로 프로젝트 폴더에서 찾아서 목록을 만들어 준다고 한다.</li>
<li><code>include_package_data</code>를 <code>True</code>로 지정하면 <code>MANIFEST.in</code>에서 지정한 패키지 디렉터리에서 찾은 데이터 파일을 자동으로 포함한다.</li>
<li><code>install_requires</code>는 설치할 때 필요한 다른 패키지의 목록이다.</li>
<li><code>setup_requires</code>는 setup script를 실행할 때 필요한 패키지 목록이다.</li>
<li><code>tests_require</code>는 테스트에 필요한 패키지 목록이다. <code>test</code> 명령어를 실행할 때 <code>setuptools</code>가 이 패키지를 가져온다.</li>
</ul>
<p>그리고 <a href="https://spoqa.github.io/2017/10/06/python-env-managers.html">파이썬의 개발 "환경"(env) 도구들</a>을 보면 다음과 같이 나와 있다.</p>
<blockquote>
<p><code>pip</code> 같은 게 없던 때에는 라이브러리 타르볼을 받아서 푼 다음 <code>python setup.py install</code> 명령을 실행하는 것이 일반적인 라이브러리 설치법이었습니다. 지금도 <code>pip</code>는 <code>*.whl</code> 파일이 아닌 <code>*.tar.gz</code>/<code>*.zip</code> 파일인 패키지를 설치할 때 내부적으로 <code>python setup.py install</code> 스크립트를 실행합니다.</p>
</blockquote>
<p>일단 여기에서 어감으로 볼 때 <code>pip</code>가 있는 지금은 <code>python setup.py install</code> 명령어를 사용할 필요가 없어 보였다. <a href="https://stackoverflow.com/questions/4324558/whats-the-proper-way-to-install-pip-virtualenv-and-distribute-for-python">Stackoverflow</a>를 보니 다음과 같이 나와 있었다.</p>
<blockquote>
<p>NO. NEVER EVER do <code>sudo python setup.py install</code> whatever. Write a <code>~/.pydistutils.cfg</code> that puts your pip installation into <code>~/.local</code> or something. Especially files named <code>ez_setup.py</code> tend to suck down newer versions of things like setuptools and easy_install, which can potentially break other things on your operating system.</p>
</blockquote>
<p><strike>정확히 이유는 모르겠지만 버전을 깨뜨릴 수 있으니 <code>python setup.py install</code>을 쓰지 말라고 하므로 일단 사용 안 하면 되겠다 싶었다.</strike> 이 내용은 정확히는 <code>python setup.py install</code>을 쓰면 안 된다기 보다는 <code>sudo</code>로 설치하지 말라는 얘기라고 한다. 글을 볼 때 <code>pip</code>와 <code>setup.py installl</code>의 차이를 검색하면서 들어갔고 라이브러리나 애플리케이션 코드는 <code>sudo</code>로 설치하지 않는 것은 보안상 일반적인 관례이므로 그 부분을 얘기한다는 것은 생각하지 못했다.(다시 읽어봐도 약간 애매하게 쓰인 듯...) 하면 안된다기보다는 이젠 <code>pip install</code>이 있어서 할 필요가 없어진 거라고 보면 된다.</p>
<p>그리고 <a href="https://github.com/pallets/flask/blob/master/examples/flaskr/README">README 문서</a>를 보면 <code>pip install --editable .</code>로 설치하라고 되어 있다. <a href="https://pip.pypa.io/en/stable/">pip</a>가 <a href="https://pypi.python.org/pypi">Python 패키지</a> 설치 도구인 것은 알고 있었는데 더 자세한 내용 확인을 위해서 <a href="https://docs.python.org/3/installing/index.html#installing-index">Installing Python Modules</a>를 참고했다.</p>
<ul>
<li><a href="https://pip.pypa.io/en/stable/">pip</a>는 현재 사용하는 인스톨 프로그램이고 Python 3.4부터는 Python에 포함되어 있다.</li>
<li><a href="https://pypi.python.org/pypi">PyPI</a>는 Python 패키지의 공개 저장소이다.</li>
<li><code>distutils</code>은 1998년에 Python 표준 라이브러리로 추가된 빌드/배포 시스템이다. 현재 <code>distutils</code>을 직접 사용하는 게 점점 줄어들고 있지만, 현재 패키징 및 배포 인프라의 토대로 남아있다.<br />
<br></li>
</ul>
<h2>pip</h2>
<p>기본 사용법은 <code>pip install PackageName</code>이다. 특정 버전을 설치하려면 <code>pip install PackageName==1.0.4</code>와 같이 버전일 지정할 수 있고 범위로 지정하려면 <code>pip install PackageName>=1.0.4</code>와 같이 사용할 수도 있다. 이미 패키지를 설치한 상황이라면 <code>pip install --upgrade PackageName</code>로 업데이트한다.</p>
<p>사용법은 파악했지만, 위에서 나온 대로 <code>pip install --editable .</code>를 할 때</p>
<ul>
<li>현재 폴더에서 설치할 때 <code>setup.py</code>와의 연관 관계를 아직 정확히 이해하지 못했다.</li>
<li><code>--editable</code>의 용도를 이해하지 못했다.</li>
<li><code>pip install -r requirement.txt</code>같은 명령어를 많이 본 적이 있는데 이 부분을 이해하지 못했다.</li>
<li><code>setup.py</code>와 <code>requirement.txt</code>의 차이를 잘 모르겠다.</li>
</ul>
<p>코드 한 줄 작성하기도 전에 모르는 게 너무 많아서 고생하고 있지만 하나씩 자료를 찾아봤다.<br />
<br></p>
<h3><code>pip install .</code></h3>
<p><code>pip install</code>의 사용법을 살펴보자.</p>
<pre class="line-numbers"><code class="language-bash">$ 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> ...
</code></pre>
<p>인자로 <code>requirement specifier</code>(아직 뭔지 모르겠지만), <code>-r</code> 옵션으로 requirements 파일, URL,디렉터리 경로를 지정할 수 있다고 나와 있다. <a href="https://pip.pypa.io/en/stable/reference/pip_install/#argument-handling">문서</a>를 보면 로컬 디렉터리를 지정할 때 반드시 <code>setup.py</code>를 지정해야 한다고 되어 있다. 위에서 <code>.</code>은 현재 디렉터리를 지정한 것이므로 여기서 <code>setup.py</code>를 찾아서 자동으로 처리해 주는 것으로 보인다.<br />
<br></p>
<h3><code>--editable</code></h3>
<p><code>--editable</code> 옵션의 <a href="https://pip.pypa.io/en/stable/reference/pip_install/#install-editable">문서</a>를 보면 editable 모드로 프로젝트를 설치한다고 한다. <a href="https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs">Editable 설치 문서</a>를 보면 setuptools develop 모드와 같다고 하고 로컬에 <code>SomeProject.egg-info</code> 디렉터리가 생기는데 <code>setup.py develop</code>보다 좋은 점은 현재 워킹 디렉터리에 바로 <code>egg-info</code>를 생성하는 부분이라고 한다. <code>egg-info</code>도 모르지만 setuptools develop 모드도 몰라서 문서를 읽어도 이해가 안 되었다.</p>
<p>이 내용은 Setuptools의 <a href="https://setuptools.readthedocs.io/en/latest/setuptools.html#development-mode">Development Mode</a>에 자세히 나와 있었는데 이해한 대로만 간단히 줄여 보자면...</p>
<ul>
<li><code>distutils</code>가 기본적으로 프로젝트의 배포판을 빌드할 것이라고 가정하기 때문에 개발하면서 변경을 할 때 다시 빌드하고 설치해야 한다.</li>
<li>동시에 두 가지 연관 프로젝트를 개발할 때 두 프로젝트를 한 디렉터리에 넣어서 실행해야 하는데 <code>distutils</code>로는 이를 할 수 없다.</li>
<li><code>Setuptools</code>에서는 공통 디렉터리나 스테이징 영역에 파일을 복사하지 않은 채로 배포할 수 있도록 지원하고 있다. 이를 이용하면 각 프로젝트에서 코드를 바로 수정해서 사용할 수 있다. 이때는 C 확장이나 컴파일된 파일을 수정할 때만 빌드하면 된다.</li>
<li><code>setup.py develop</code> 명령어를 사용하면 <code>setup.py install</code>과 비슷하게 동작하지만, 아무것도 설치하지 않는다. 대신 <code>.egg-link</code>를 생성하고 이를 프로젝트의 소스코드와 연결한다. 배포 디렉터리가 <code>site-packages</code> 디렉터리라면 소스코드를 포함하기 위해 <code>easy-install.pth</code>를 수정해서 <code>sys.path</code>에서 사용할 수 있게 한다.</li>
</ul>
<p>아직 개발을 안 해봐서 의존성 설치와 현재 프로젝트의 코드 수정이 어떻게 연결되는지까지는 정확히 이해하지 못했지만 <code>editable</code>모드가 어떤 의미인지는 이해했다. 수정할 때마다 빌드하지 않게 한다는 용도로 이해했는데 이는 나중에 실제로 flask 애플리케이션을 개발하면서 겪어봐야 더 이해가 될 것 같다. <code>--editable</code> 옵션 없이 사용해보면 불편한 부분을 눈치채지 않을까 생각한다.</p>
<p>실제로 위 <code>setup.py</code> 파일로 <code>pip install --editable .</code>을 실행하면 아래의 파일이 새로 생긴다.</p>
<pre class="line-numbers"><code class="language-bash">├── .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
</code></pre>
<p>이 파일 중에서 <code>demo.egg-link</code>를 보면 현재 폴더의 경로인 <code>/Users/outsider/demo</code>로 지정되어 있고 <code>easy-install.pth</code>도 같은 경로로 지정되어 있다.(지정되어 있다기보다 txt 파일이라 내용에 저 경로가 있다.) <code>sys.path</code>가 달라지진 않아서 정확한 동작 방식까지는 이해 못했지만 앞에서 이해한 내용대로 동작하기 위해서 만들어진 것 정도는 이해했다.</p>
<p>앞에서 Eggs 포맷에 대해서 간단히 살펴봤는데 실제로 관련 파일이 생기니까 <code>.eggs</code>와 <code>.egg-info</code>가 무엇인지 궁금해졌다. 이 부분은 <a href="http://setuptools.readthedocs.io/en/latest/formats.html">The Internal Structure of Python Eggs</a>에 자세히 나와 있었다.</p>
<ul>
<li><code>.egg</code>는 프로젝트의 코드와 리소스를 담고 있는 디렉터리나 zip 파일로 프로젝트의 메타데이터를 가진 EGG-INO 서브디렉토리 옆에 있는다.</li>
<li><code>.egg-info</code>는 프로젝트의 코드와 리소스 옆에 있는 파일이나 디렉터리로 프로젝트의 메타데이터를 담고 있다.</li>
</ul>
<p><code>.eggs</code> 폴더 안에 <code>setup_requires</code>로 지정된 <code>pytest_runner</code>의 <code>egg</code> 파일이 있는 것으로 보아 설치한 내용이 여기에 담긴 것 같다. <code>--editable</code> 옵션이 없이 설치해보면 <code>.eggs</code>와 <code>.egg-info</code>가 생기지 않는 것으로 보아 Editable 모드로 실행하는 데 필요한 것으로 보인다. (아직 site-packages 안에 생긴 것과 <code>.eggs</code> 안에 생긴 것의 차이는 잘 모르겠다.)</p>
<p>이 파일들은 VCS에서 관리할 필요가 없으므로 <code>.eggs/</code>, <code>*.egg-info/</code>는 <code>.gitignore</code> 등으로 제외했다.<br />
<br></p>
<h3><code>requirements.txt</code></h3>
<p>Flask 예제 프로젝트를 보고 <code>setup.py</code>에 지정된 의존성을 이용해서 의존성을 설치했지만, 그동안 Python 프로젝트를 보면서는 <code>pip install -r requirement.txt</code>처럼 사용하는 것을 훨씬 더 많이 봤다. 그렇다 보니 이 둘의 차이가 궁금해졌고 의존성을 어느 쪽에서 관리하는 게 좋은지도 궁금해졌다.</p>
<p>pip의 <a href="https://pip.pypa.io/en/stable/user_guide/#requirements-files">Requirements Files</a>를 보면 <a href="https://caremad.io/posts/2013/07/setup-vs-requirement/">setup.py vs requirements.txt</a>라는 글이 링크되어 있는데 다행히도 <a href="http://www.haruair.com/blog/3719">setup.py와 requirements.txt의 차이점과 사용 방법</a>으로 번역이 되어 있다.</p>
<ul>
<li><code>setup.py</code>에는 PyPI에 배포할 라이브러리를 만들 때 의존성을 지정하고 <code>requirements.txt</code>는 서버 등에 배포할 때 의존성을 지정하는데 사용한다.</li>
<li><code>setup.py</code>는 추상화된 의존성을 의미하고 <code>requirements.txt</code>는 실제 특정 라이브러리를 지정하는데 사용한다.</li>
<li>PyPI가 아닌 다른 곳에 패키지를 올려놓고 사용할 때 <code>requirements.txt</code>에서 받을 곳을 지정해서 사용할 수 있다.</li>
<li>추상 의존성과 구체적 의존성을 나누어서 사용할 때의 좋은 점은 공개된 라이브러리를 수정해서 사용할 때 <code>requirements.txt</code>에서 다른 버전을 사용하도록 지정해서 사용할 수 있다.</li>
</ul>
<p>이 글을 읽어보니 <code>setup.py</code>와 <code>requirements.txt</code>를 둘 다 사용하면서 의존성을 관리하고 개발자는 <code>pip install -r requirements.txt</code>로 설치해서 사용하도록 안내하는 게 좋은 방법이라고 생각되었다.(Flask 예제에서는 그렇게 하고 있지 않지만...) 일단 이렇게 관리하기 시작한 뒤에 필요한 경우 <code>requirements.txt</code>에서 다른 곳에서 받아오도록 지정할 수 있다.</p>
<p><code>pip freeze > requirements.txt</code> 명령어를 이용해서 현재 로컬에 설치된 패키지 기준으로 <code>requirements.txt</code>를 만들어서 사용할 수 있지만 윗글에서 나온 대로 <code>requirements.txt</code>를 다음과 같이 만들어서 사용했다. PyPI에 올라온 라이브러리를 직접 수정해서 사용할 일은 근래에는 없을 것 같으므로 추가하는 의존성은 <code>setup.py</code>에서 지정해서 사용하면 될 것 같다.</p>
<pre class="line-numbers"><code class="language-bash">$ cat requirements.txt
--index-url https://pypi.python.org/simple/
-e .
</code></pre>
<p>이를 이용해서 재설치를 해보자 정상적으로 잘 설치가 된다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<h2>setup.cfg</h2>
<p>예제 프로젝트인 <a href="https://github.com/pallets/flask/tree/master/examples/flaskr">flaskr</a>에는 <a href="https://github.com/pallets/flask/blob/master/examples/flaskr/setup.cfg">setup.cfg</a>도 있었는데 이에 대해서는 <a href="https://docs.python.org/2/distutils/configfile.html">Writing the Setup Configuration File</a>에 나와 있었다.</p>
<pre class="line-numbers"><code class="language-clike">[aliases]
test=pytest
</code></pre>
<p><code>setup.cfg</code>의 내용은 위와 같았는데 위 문서를 보면 그 형태는 다음과 같다.</p>
<pre class="line-numbers"><code class="language-clike">[command]
option=value
</code></pre>
<p>이는 커맨드라인 명령어를 사용할 때 해당 명령어에 옵션을 자동으로 제공하기 위해서 사용하거나 기본값을 제공하기 위해서 사용한다고 한다. 자세한 내용에 대해서는 잘 못 찾겠기는 한데 위 내용이나 <a href="https://github.com/pallets/flask/blob/master/setup.cfg">Flask의 setup.cfg</a>의 내용을 볼 때 <code>setup.py</code>에서 명령어를 실행할 때 참고정보를 여기에 넣어서 사용하는 것으로 보인다. 그래서 위에 별칭이 <code>test=pytest</code>로 되어 있는 것으로 보아 <code>test</code> 명령어를 실행하면 <code>pytest</code>를 실행하라는 정도의 의미로 보였다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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 ===================
</code></pre>
<p>실제로 해보니 위 명령어로 <code>pytest</code>가 잘 실행되었고 <code>python setup.py pytest</code>로 실행해도 결과는 똑같았다.<br />
<br></p>
<h2>MANIFEST.in</h2>
<p>Flask 예제를 볼 때 용도를 몰라서 넘어갔던 파일이 <a href="https://github.com/pallets/flask/blob/master/examples/flaskr/MANIFEST.in">MANIFEST.in</a>이다. <code>setup.py</code>의 <code>include_package_data</code>를 볼 때 여기서 이 파일에 지정한 디렉터리의 데이터 파일을 자동으로 포함한다고 하는데 정확한 용도는 모르겠다. 예시의 <code>MANIFEST.in</code>는 아래와 같이 되어 있다.</p>
<pre class="line-numbers"><code class="language-clike">graft flaskr/templates
graft flaskr/static
include flaskr/schema.sql
</code></pre>
<p>이건 실제로 Flask 애플리케이션 개발을 해야 제대로 이해할 수 있을 것 같아서 일단은 개발환경에는 바로 넣지 않았다.<br />
<br></p>
<p>이제 Flask 애플리케이션 코드를 작성해 볼 수 있게 되었다.(한꺼번에 너무 많은 개념을 보느라 잘못된 부분도 있을 것 같지만...)<br />
<br><br></p>
<h1>글을 쓴 이후로 SNS에서 피드백을 받은 내용이 있어서 추가로 내용을 보충합니다.(2017.10.09)</h1>
<p><code>wheel</code>과 <code>eggs</code> 관련:</p>
<ul>
<li><a href="https://wheel.readthedocs.io/en/latest/">wheel</a>(<code>*.whl</code>)은 <a href="http://peak.telecommunity.com/DevCenter/PythonEggs">Eggs</a>(<code>*.egg</code>)를 대체하려고 나온 것이고 현시점에서 거의 대체된 것으로 보인다고 한다. 그래도 setuptools의 내부에서는 아직 Eggs를 사용하는 것으로 보인다.</li>
<li>C/C++ 코드를 포함하는 패키지를 위한 포맷을 "bdist"라고 부른다. 빌드된 것을 배포하므로 설치할 때 C/C++ 컴파일러가 없어도 된다.</li>
<li>C/C++ 코드가 있더라도 소스 코드 형태로 압축해서 배포하는 것으로 "sdist"라고 부른다. 그래서 설치하는 쪽에서 C/C++ 컴파일러가 없으면 빌드할 수 없으므로 설치할 수 없다.</li>
<li>C/C++ 코드가 전혀 없으면 bdist, sdist 어느 쪽으로 배포해도 상관없다. </li>
<li>bdist 이름에는 OS, CPU 종류, libc 버전 등의 태그가 붙어서 <code>pip</code>로 설치할 때 부합되는 bdist를 선택해서 설치한다. <a href="https://pypi.python.org/pypi/libsass/0.13.2#downloads">libsass의 배포 파일</a>을 보면 64비트 인텔 프로세스 Windows에서는 <code>pip install libsass==0.13.2</code>을 했을 때 해당 플랫폼용 bdsit가 있으므로 이를 받아서 압축을 푸는 것으로 설치가 끝나지만 FreeBSD에서 설치한다면 bdist가 없으므로 <code>pip</code>가 sdist를 다운받아서 C/C++ 코드를 직접 빌드하게 된다.</li>
</ul>
<p><code>requirements.txt</code>와 <code>setup.py</code> 관련:</p>
<ul>
<li>둘 중 어느 쪽으로 패키징 할지 <code>requirements.txt</code>를 관리할 때 직접 할지 <code>pip freeze</code>로 할지는 아닌 많은 논의가 있다.(현재 정해진 방법은 없다.)</li>
<li>파이썬 라이브러리를 만들면 <code>setup.py</code>를 쓰면 된다.</li>
<li>실제로 해보면서도 라이브러리로 배포하는 게 아니라면 <code>setup.py</code>가 필요한가 하는 의문이 있었는데 실제로 관련 논의가 많이 있는 것 같고 웹 애플리케이션의 경우 <code>requirements.txt</code>로만 관리하기도 한다고 한다. </li>
<li>CLI 명령어를 제공해야 할 때에는 <code>setup.py</code>를 사용하는 이점이 있다. </li>
<li><code>setup.py</code>는 일반 의존성 목록을 관리할 때 쓰고 <code>requirements.txt</code>는 <code>pip freeze</code>로 만들어서 <code>lockfile</code>처럼 쓰는 방법도 있다.</li>
</ul>
<p>이건 개발해보면서 어느 쪽에 이점이 있는지 다양하게 시도해봐야 할 것 같다. 약속된 관례가 있는 것은 아니라는 것을 확인한 것도 큰 수확이다.(사실 이게 꽤 궁금했다.)</p>
<p>Flask 관련:</p>
<ul>
<li>CLI로 Flask 앱을 실행하는 방식이 예전에는 없었던 방식이라는 걸 알았다.</li>
<li>Flask 문서를 보면 어디서는 <code>flask</code> CLI로 실행하고 어느 문서에서는 <code>if __name__ == '__main__': app.run()</code>같은 코드를 하단에 두어서 <code>python hello.py</code>로 실행하고 있다. 대문에서 CLI로 안내하고 있어서 이 글은 그대로 적었는데 후자의 방법이 감춰진 마법이 적어서 이해하기 좋을 것이라고 한다.</li>
</ul>
<p><strong><a href="https://blog.outsider.ne.kr/1325?commentInput=true#entry1325WriteComment">댓글 쓰기</a></strong></p>Python 개발환경 구성Outsiderhttps://blog.outsider.ne.kr/13242017-10-08T15:26:05+09:002017-10-08T15:26:05+09:00<p>Python을 못하면서도 Python이 주 언어인 회사로 <a href="https://blog.outsider.ne.kr/1233">이직</a>한지 1년이 넘었지만 나는 여전히 Python을 거의 못한다. 실제로는 Python + Django까지 알아야 하지만 어쨌든 못한다. 핑계(?)를 대자면 신입이 아니다 보니 공부만 하고 있을 수는 없고 월급 받는 값은 해야 해서 내가 할 수 있는 일 위주로 하다 보니 애플리케이션은 주로 Node.js를 사용했고 AWS 인프라 관련 쪽에 좀 더 집중하고 있었다.</p>
<p>코드 리뷰를 해야 해서 Django 코드를 계속 보긴 했는데 직접 깊숙이 개발하는 건 아니다 보니 리뷰의 깊이는 한없이 얕았다. 이제 Django 프로젝트에 더 많이 관여해야 하므로 Python을 공부해야겠다 싶어서 개인 프로젝트를 Pyhton으로 올리기로 했다. 그동안도 계속 생각은 했지만, 개발 속도 면에서 아무래도 답답하다 보니 고민하다가 Node.js를 선택하곤 했다.<br />
<br></p>
<h1>새로운 언어의 학습</h1>
<p>작년부터 Python을 공부해야겠다고 생각한 뒤로 <strong>내가 새로운 프로그래밍 언어를 새로 배운지가 엄청 오래되었다는 걸 깨닫게 되었다.</strong> ASP도 해봤고 Java도 해봤고 JavaScript도 해봤고 새로운 기술을 좋아하는 탓에 무수한 프레임워크와 도구를 갈아타면서 새로운 것을 학습하는 데 어느 정도 자신 있다고 생각했는데 언어를 새로 배운 것은 아주 예전에 일이었다. Node.js를 하긴 하지만 Node.js는 런타임일 뿐이고 JavaScript는 신입 때부터 배웠으니 사실상 Java를 배워서 실무에 써먹기 시작한 뒤로는 새로운 언어를 배운 적이 없었다. 물론 전혀 몰랐다는 건 거짓말이겠고 2~3년 전부터 너무 JavaScript 세상에 갇혀 있는 것 같아서 걱정하고 있긴 했다.</p>
<p>이 부분을 깨닫고 나자 "나는 새로운 언어를 어떻게 공부했더라?" 하는 생각이 들었다. 그래서 Python 공부를 계속 미뤘는지도 모르겠다. 프로그래밍 언어에 많은 관심이 많은 편은 아니라서... 일단 <a href="https://blog.outsider.ne.kr/1296">Python 책을 추천받아서 읽었다</a>. 다 읽고 나서 예전에는 아는 게 없어서 이렇게 책으로 공부했지만. 이제는 프로그래밍 언어 책을 본다고 그 언어로 프로그래밍을 할 수 있는 게 아니라는 걸 알게 되었다.</p>
<p>책에는 언어의 사용법이 자세히 나와 있었지만 읽는 내내 사실 좀 지루했다. 한번 훑어볼 가치는 있다고 생각하지만 읽어본다고 다 외워지는 것도 아니고 사용법은 사용하면서 익숙해지면 된다는 생각이 들었다. 예전에 배울 때는 어차피 아는 게 없었으므로 그냥 공부했던 것 같은데 지금은 현재 익숙한 환경에서 가진 지식과 어설프게 주워들어서 알고 있는 지식이 섞이면서 어떤 식으로 배워야 할지가 더 어렵게 느껴졌고 책에 나오지 않는 많은 부분이 궁금해지면서 코드를 쉽게 작성하기 어려웠다.</p>
<ul>
<li>실무에서 프로젝트를 구성할 때 환경 설정은 보통 어떻게 하지? Python은 버전도 여러 버전을 사용하는데 이 관리는 어떻게 할까?</li>
<li>Python 프로젝트에서 보이는 내가 이해 못 하는 이 파일들의 용도는 무엇일까?</li>
<li>의존성 관리는 보통 어떤 식으로 하는가?</li>
<li>pip로 설치를 하면 이 파일들은 어디에 들어가서 관리가 되는 거지?</li>
<li>Python 개발하면서 많은 도구가 보이는데 어떤 것을 써야 하는 거고 왜 필요한 거지?</li>
<li>폴더구조나 파일 및 코드 작성에 대한 베스트 프렉티스와 안티패턴은 어떻게 되는 거지? 이런 부분은 Python의 동작 방식에 대한 지식이 많지 않으므로 더 어렵게 느껴졌다.</li>
</ul>
<p>오히려 예전보다 지식이 쌓였기 때문인지, 모르는 게 너무 많아서 오히려 시작하기가 어려운 느낌이었다. 예를 들어 Node.js의 경우 V8이 어떻게 동작하는지 알고 있으므로 코드를 작성할 때 이런 부분을 염두에 두고 작성하고 npm으로 패키지를 설치할 때도 어떻게 관리하는 게 좋고 어디에 설치되고 재설치할 때는 어떻게 하는지, 어떤 파일을 VCS로 관리해야 하는지에 대한 이해가 있는데 Python에서는 궁금한 건 산더미인데 가진 지식이 없다 보니 잘 시작이 안 되었었다. 일단 실무에서 사용 중인 프로젝트를 손대려고 하다 보니 더 그럴 수도 있다.</p>
<p>물론 자료가 없는 건 아니고 수많은 자료가 있지만 나는 전체 그림을 이해하고 상세를 이해하는 편이라서 그 수많은 자료를 이해하려면 전체를 파악할 수 있는 사전 지식이 좀 더 필요하다고 생각했다. 사실 코드를 작성하려면 못할 것도 없고 업무로 프로젝트 투입되어 2~3달 내에 웹사이트라도 만들어야 한다면 튜토리얼 보면서 대충 눈치껏 작성한다면 작성하겠지만 내가 하고 싶은 건 당장 Python으로 뭔가를 만드는 게 아니라(이미 뭔가 만들 수단은 가지고 있으므로) Python을 배우려는 것이므로 뭔가 차근차근히 해보려다가 미루기만 했던 것 같다.</p>
<p><strong>결국, 이런 정보는 직접 삽질해보면서 습득하는 수밖에 없어서 사이드 프로젝트를 강제로 Python으로 진행해 보기로 했다.</strong> 그동안은 사이드 프로젝트도 익숙한 언어나 플랫폼으로 자꾸 하게 돼서 이번에는 강제로(?) Python을 선택했다. 사이드 프로젝트가 완성될지 미지수이지만 익숙한 언어로 해도 완성 안 되기는 마찬가지니까... 더불어 하나씩 해보다 보니 궁금해서 찾아보는 것도 많아질 것 같아서 공부가 많이 필요한데 그렇게 배운 내용을 정리해 보면 주변의 Python 개발자분들이 내가 잘못 이해한 걸 바로잡아주지 않을까 싶어서 글로 적게 되었다.<br />
<br></p>
<h1>pyenv</h1>
<p>Python만 그런 건 아니지만 프로그래밍 언어도 버전이 계속 올라가지만 모든 프로젝트를 같은 버전으로 맞추기는 어려우므로 보통 버전 관리자를 사용한다. Java 할 때는 프로젝트별로 버전을 다르게 쓴 기억은 별로 없어서 <code>export JAVA_HOME=/usr/libexec/java_home -v 1.8</code> 처럼 환경변수로 관리하거나 IDE에서 설정해서 썼던 것 같다. Node.js 같은 경우는 버전이 상당히 자주 올라가기 때문에 <a href="https://github.com/creationix/nvm">nvm</a>이나 <a href="https://github.com/tj/n">n</a>같은 관리 도구를 사용한다. 나는 이런 도구는 한 번도 사용해 본 적이 없고 보통 Node.js 소스코드에서 빌드해서 심볼릭 링크로 바꿔가면서 사용한다. 얼마 전에 왜 그렇게 하냐는 질문을 받았는데... 음... 저런 도구가 없을 때부터 하던 습관이라고 밖에는 할 말이 없긴 했다.</p>
<p>어쨌든 이런 접근 방법은 사용하면서 조금씩 다듬어진 방법인데 Python은 크게 2.x와 3.x가 있으므로 Python 버전 관리자는 중요했고 회사 프로젝트도 프로젝트마다 버전이 달라져 있으므로 README에 적어두더라도 한번 환경설정을 해두면 해당 프로젝트에 들어갔을 때 자동으로 버전이 선택되고 확인할 수 있어서 크게 신경 쓰지 않기를 바랐다. Ruby에는 <a href="https://rvm.io/">RVM</a>이나 <a href="https://github.com/rbenv/rbenv">rbenv</a>가 있는데 Python에는 <a href="https://github.com/pyenv/pyenv">pyenv</a>가 있다. 이는 달리 고민할 도구들이 없는 거 같아서 큰 고민 없이 pyenv를 사용했다.</p>
<p><a href="https://github.com/pyenv/pyenv">pyenv</a>는 간단히 말하면 한 로컬머신 내에서 여러 Python 버전을 쉽게 바꿔가면서 사용할 수 있게 해주는 도구다. macOS에서는 Homebrew로 <code>brew install pyenv</code>처럼 설치할 수 있다.</p>
<p>설치가 끝났으면 <code>~/.bash_profile</code> 파일에 <code>export PYENV_ROOT="$HOME/.pyenv</code>로 <code>PYENV_ROOT</code> 환경변수를 설정해 주고 마지막에 <code>eval "$(pyenv init -)"</code>를 추가해서 터미널에서 pyenv가 동작하도록 해야 한다. 이제 <code>pyenv</code> 명령어를 실행하면 버전과 명령어를 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<p><code>pyenv versions</code> 명령어로 설치된 버전을 확인할 수 있다. <code>*</code> 표시가 있는 버전이 현재 사용 버전이다. 현재 활성화된 버전은 <code>pyenv version</code>으로도 확인할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ pyenv versions
system
2.7.13
* 3.6.0 (set by /Users/outsider/.pyenv/version)
3.6.2
</code></pre>
<p><code>system</code>은 내 macOS에 기본으로 설치된 버전이고 다른 버전들은 내가 설치한 버전이다. 새로운 버전을 설치하고 싶다면 <code>pyenv install VERSION</code> 명령어를 사용하면 된다. 설치 가능한 버전은 <code>pyenv install --list</code>로 설치 가능한 버전을 확인할 수 있고 새로운 버전이 보이지 않는다면 <code>brew update && brew upgrade pyenv</code>로 업그레이드해야 한다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<p>위는 2.7.14를 새로 설치한 것이고 아래 버전 확인에서 추가된 것을 볼 수 있다. 보통 로컬에서 기본으로 사용하는 버전이 있으므로 <code>pyenv global 3.6.2</code>처럼 지정하면 3.6.2를 전역으로 사용하게 된다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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)
</code></pre>
<p>이제 프로젝트에서 다른 Python 버전을 사용하도록 설정해 보자. 프로젝트의 루트 폴더에서 <code>pyenv local VERSION</code>을 실행하면 된다.</p>
<pre class="line-numbers"><code class="language-bash">$ pyenv local 2.7.14
$ python --version
Python 2.7.14
</code></pre>
<p><code>python local</code> 명령어를 실행하면 현재 폴더에 <code>.python-version</code> 파일이 생기는데 여기에 버전이 지정되어 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ cat .python-version
2.7.14
</code></pre>
<p><code>pyenv</code>가 이 파일을 확인해서 해당 폴더에 들어오면 지정한 버전으로 바꾸어주므로 한번 설정한 후에는 해당 프로젝트에서 지정한 Python 버전을 사용할 수 있다. 프로젝트에서 Python 버전은 같게 써야 하지만 특정 도구의 파일을 공유할 필요는 없으므로 <code>.python-version</code>는 <code>.gitignore</code>등에 추가해서 VCS가 관리하지 않도록 한다.<br />
<br></p>
<h1>virtualenv</h1>
<p>프로젝트를 하면 관련 패키지를 사용해야 하는데 같은 패키지라도 프로젝트마다 버전이 다르므로 프로젝트별로 이 환경을 격리해야 한다. 이 문제를 해결하는 도구가 <a href="https://virtualenv.pypa.io/en/stable/">virtualenv</a>다. 나는 이런 류에 도구에는 익숙지 않다. Node.js는 기본적으로 현재 프로젝트 하위의 <code>node_modules</code>로 관리해주므로 별도로 격리하지 않아도 되고 Java에서도 클래스 패스에 의존 패키지를 두어야 하므로(아마도? Java는 이제 기억이 가물가물...) 별도로 격리 도구를 사용해 본 적이 없다. 아마 RubyGems도 Python과 비슷하지 싶은데 Ruby도 잘 몰라서 rbenv같은 도구가 해주는지 어떤지 자세히 모르겠다.</p>
<p>먼저 격리가 필요하다는 말은 이런 도구가 없으면 격리를 해주지 않는다는 의미이므로 원래는 어떻게 동작하는가를 알아야 했다. <code>virtualenv</code>로 격리하지 않으면 파이썬 버전 폴더 아래 <code>site-packages</code>안에 의존 패키지를 설치하게 된다. 이 폴더를 전역으로 공유해서 사용하므로 다른 프로젝트에서 같은 패키지의 다른 버전을 사용하려고 하면 문제가 발생하게 된다.</p>
<p>다음은 macOS의 기본 Python에서 모듈을 찾는 경로를 가진 <a href="https://docs.python.org/3.6/library/site.html">site</a> 모듈을 출력한 것이다. <code>-m</code>은 모듈을 스크립트로 바로 실행하는 옵션이다.(<code>site</code>라는 용어의 의미가 확 와닿지는 않는데 현재 실행환경 혹은 프로젝트 정도의 의미로 현재 이해하고 있다.)</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<p>테스트해보면 모듈을 설치하면 위 경로 중 <code>/Library/Python/2.7/site-packages</code>에 설치가 된다. 그리고 <code>pyenv</code>로 Python 버전을 선택하면 당연히 이 <code>site</code> 경로도 바뀌게 된다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<p>여기서는 모듈이 위 경로 중 <code>/Users/outsider/.pyenv/versions/2.7.14/lib/python2.7/site-packages</code>에 설치가 된다.<code>site-packages</code>와 관련해서 <a href="https://www.python.org/dev/peps/pep-0370/">PEP 370</a>가 있고 위에서 <code>USER_SITE</code>라는 정보가 나와서 처음에는 여기에 설치되는 줄 알았는데 이 폴더는 존재하지 않아서 정확히 <code>USER_SITE</code>의 의미를 아직 잘 모르겠다.</p>
<p>어쨌든 pyenv를 사용하더라도 위처럼 같은 python 버전 내에서는 같은 위치에 모듈을 모두 설치하므로 버전이 충돌하는 문제는 여전히 발생한다. 이를 <code>virtualenv</code>로 해결할 수 있는데 이름 그대로 프로젝트별로 가상환경을 만들어서 격리해준다. <code>virtualenv</code> 외에 <a href="https://virtualenvwrapper.readthedocs.io/en/latest/">virtualenvwrapper</a>나 <a href="https://github.com/kennethreitz/pipenv">pipenv</a>가 있고 pyenv에도 <a href="https://github.com/pyenv/pyenv-virtualenv">pyenv-virtualenv</a> 플러그인이 있는데 Python에서 의존성 관리 문제를 겪어보지 않은 나로서는 각 도구의 장단점이나 차이점을 쉽게 파악하기 어려웠다. 주변에 물어보니 처음에는 그냥 <code>virtualenv</code>를 직접 쓰는 게 동작 방식을 이해하기 좋다고 해서 그냥 <code>virtualenv</code>를 쓰기로 했다.</p>
<p><code>pip install virtualenv</code>로 <code>virtualenv</code>를 설치한다. <code>pyenv</code>에서 사용하는 버전마다 설치해 주어야 하는 것으로 보인다.</p>
<p>이제 프로젝트 루트에 가서 <code>virtualenv ENV_NAME</code>으로 가상환경을 생성한다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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.
</code></pre>
<p>여기서는 환경 이름을 <code>venv</code>로 주었으므로 현재 폴더에 <code>venv</code>라는 폴더가 생기고 그 아래 <code>virtualenv</code>를 위한 파일이 아래와 같이 생성된다.</p>
<pre class="line-numbers"><code class="language-bash">└── 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
</code></pre>
<p>당연히 이 파일을 VCS에 추가하지 말아야 하는데 일반적인 관례로 <code>venv</code>나 <code>env</code>를 쓰는 것 같다.(확실하지는 않다.) 이건 생성만 한 것이므로 <code>source ENV_NAME/bin/activate</code> 명령어로 이 가상환경을 활성화해야 한다. 활성화되면 프롬프트 앞에 환경 이름이 표시돼서 활성화되었음을 확인할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<p><code>site</code> 패키지를 확인해 보면 <code>/Users/outsider/temp/pyenv-test/venv/lib/python3.6/site-packages</code>로 현재 프로젝트 하위에 지정되어 있을 것을 볼 수 있다.<br />
<br></p>
<h1>pyvenv</h1>
<p>Python 환경을 구성하는 도구를 검토하기가 어려워서 SNS에 질문을 올렸더니 <a href="https://twitter.com/hongminhee">홍민희</a>님이 관련 도구의 역사와 용도를 <a href="https://spoqa.github.io/2017/10/06/python-env-managers.html">블로그에 정리</a>해서 올려주셨다. 이 글을 통해서 Python 개발환경의 역사와 각 도구의 필요성을 이해할 수 있었는데 Python 3.3부터 <a href="https://docs.python.org/3/library/venv.html">pyvenv</a>라는 이름으로 <code>virtualenv</code>가 내장되었다는 것을 알게 되었다. 그래서 위에서는 <code>virtualenv</code>를 설치해서 사용했지만, Python 3.3 이상을 사용한다면 굳이 따로 설치하지 않고 내장된 <code>pyvenv</code>를 사용하는 게 좋겠다는 생각이 들었다.</p>
<p><a href="https://docs.python.org/3/library/venv.html">venv</a>문서와 <a href="https://www.python.org/dev/peps/pep-0405/">PEP 405 -- Python Virtual Environments</a>를 보면 3.3에 도입되어 원래는 <code>pyvenv</code>라는 명령어가 있었지만 <a href="https://docs.python.org/dev/whatsnew/3.6.html#deprecated-features">Python 3.6부터는 deprecated</a>되고 <code>python -m venv</code>를 사용하기를 권장하고 하고 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ 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
</code></pre>
<p>현재 프로젝트 폴더에서 Python 3.6 이상으로 사용한다고 할 때 <code>python -m venv ENV_NAME</code>으로 새로운 <code>virtualenv</code>를 생성할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ python -m venv venv
</code></pre>
<p><code>venv</code>가 2번 있어서 애매한데 뒤에 있는 <code>venv</code>가 환경의 이름이다. 아래는 <code>venv</code>의 폴더 구조인데 앞에서 <code>virtualenv</code>로 만들어진 파일과 약간 다르긴 한데 사용상에 큰 차이는 없어 보인다.</p>
<pre class="line-numbers"><code class="language-bash">└── 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
</code></pre>
<p>현재는 Python 3.6 이상에서 <code>python -m venv</code>를 사용하고 그 밑의 버전에서는 <code>virtualenv</code>를 설치해서 사용하려고 한다.</p>
<p>여기서는 프로젝트 폴더에 들어가서 <code>virtualenv</code>를 활성화했지만 이는 폴더 기반으로 동작하는 것은 아니다. 폴더에 진입할 때 자동으로 활성화를 하려면 다른 도구를 써야 하는데 나는 <a href="https://blog.outsider.ne.kr/1306">direnv를 사용 중</a>이므로 <code>direnv</code>로 <a href="https://github.com/direnv/direnv/wiki/Python"><code>Python</code>을 연동</a>해서 사용하고 있다. 3.6 이상에서는 <code>python -m venv</code>를 사용했지만, 내부는 <code>virtualenv</code>가 내장된 것이므로 <code>direnv</code>와 연동해서 사용하는데도 큰 문제가 없다. 각 도구가 역할을 잘 하고 있으므로 Python 버전 관리는 <code>pyenv</code>에 맡기고 <code>direnv</code>의 <code>.envrc</code>에서는 <code>layout virtualenv venv</code>처럼 지정해서 <code>virtualenv</code> 활성화만 할 수 있도록 했다.</p>
<p><a href="https://spoqa.github.io/2017/10/06/python-env-managers.html">홍민희 님의 글</a>에서는 "파이썬 프로그래머이고, 여러 애플리케이션을 다양한 파이썬 버전으로 개발"하는 경우 <code>pyenv-virtualenvwrapper</code>를 추천하고 있지만 <code>pyvenv</code>를 써보니 직관적이고 간단해서 아직은 <code>virtualenvwrapper</code>가 필요한 이유가 잘 떠오르지 않는다. <code>node_modules</code>와 비슷하게 동작해서 그렇게 느껴질 수도 있지만 <code>direnv</code>와도 연동하고 나니 프로젝트 구성할 때만 한번 해두면 신경 쓰지 않아도 되어서 매우 편하게 느껴졌다.<br />
<br><br></p>
<p>이제 Python 프로젝트를 진행하기 위한 기본 환경 구성이 끝났다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1324?commentInput=true#entry1324WriteComment">댓글 쓰기</a></strong></p>