Outsider's Dev Story

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

Flask 공식 튜토리얼 따라하기 #1

Flask 개발환경을 구성했으므로 이제 Flask 애플리케이션을 개발할 차례가 되었다. 문서를 보면서 대충 눈치껏 만들어 보려고 했지만, 막상 만들려고 하니까 모르는 부분이 너무 많았다.

  • 프로젝트 폴더/파일 구조는 어떻게 가져가야 하는가?
  • 웹 애플리케이션의 진입점이 되는 라우팅 처리와 HTTP 메서드 처리는 어떻게 하는가?
  • 뷰 템플릿인 Jinja2는 어떻게 사용해서 페이지를 제공할 수 있는가?
  • HTML을 반환할 때와 JSON을 응답할 때는 어떻게 처리해야 하는가?(content negotiation?)
  • DB 연동을 어떻게 해야 하는가?(추가로 SQL Alchemy?)
  • 정적 파일은 어떻게 제공하는가?
  • Flask를 설치할 때 Werkzeug, itsdangerous, click 패키지를 함께 설치하는데 어떤 패키지일까?
  • 테스트는 어떻게 작성하는가?

예제 코드를 보고 해보다가 안 되겠다는 느낌이 들어서 Python 환경에 대해서 궁금한 걸 하나하나 정리하고 나니 이해하기가 한결 편해져서 Flask도 튜토리얼부터 차근차근히 해보기로 했다. 중간중간 자꾸 대충 이해할 것 같은 기분에 건너뛰고 싶은 충동이 느껴져서 이번에도 모르는 부분을 다 찾아보면서 일단 Flask 애플리케이션 개발 및 Python 개발에 대해서 좀 더 이해해 보기로 했다.

튜토리얼은 Flask로 간단한 블로그 애플리케이션 flaskr을 만드는 과정을 설명한다. 이 블로그는 설정해 놓은 사용자로 로그인을 할 수 있고 사용자는 제목과 내용(HTML)으로 글을 올릴 수 있고 첫 페이지에서 올려진 글 목록을 보여준다.

0 단계: 폴더 생성

└── flaskr/
    └── flaskr/
        ├── static/
        └── templates/

튜토리얼에서는 위와 같은 폴더 구조를 제안하고 있다. 이게 Flask의 관례인지 Python의 관례인지는 아직 잘 모르겠지만 프로젝트명으로 된 루트 디렉터리 아래 같은 이름에 디렉터리가 또 있다는 부분 그러니까 flaskr/flaskr의 형태가 좀 어색하게 느껴졌다. src같은 게 더 자연스럽지 않나 생각하는데 느낌상 배포 등을 할 때 이름을 지정하는 것과 관련이 있지 않을까 상상 정도를 해본다. 그러고 보니 회사에서 보던 Django 프로젝트도 저런 형태의 폴더구조를 본 기억이 났다. FLASK_APP=flaskr.factory:create_app() 처럼 환경변수를 지정할 때 FLASK_APP=src.factory:create_app() 같은 형태가 되면 이상하니까 그런 게 아닐까 정도의 생각이다.

여기서 static/ 폴더는 .js.css 같은 정적 파일을 모아두는 폴더고 templates/Jinja2의 뷰 파일을 두는 폴더다.

1 단계: 데이터베이스 스키마

drop table if exists entries;
create table entries (
  id integer primary key autoincrement,
  title text not null,
  'text' text not null
);

위 내용으로 flaskr/flaskr/schema.sql 파일을 생성한다. 블로그에서 사용할 간단한 테이블이다. Alembic같은 마이그레이션 도구를 사용하는 것은 아니지만 drop 테이블이 있는 것으로 보아 마이그레이션 도구와 비슷하게 사용하려는 것으로 보인다. 참고로 이 튜토리얼에서는 SQLite를 쓰는데 나는 SQLite로 애플리케이션을 만드는 경우가 거의 없어서 크게 관심 없지만 일단 튜토리얼이라서 그대로 따라 해보고 있다.

2 단계: 애플리케이션 설정 코드

여기서는 애플리케이션을 설정하는 코드를 작성한다. flaskr/flaskr 아래 flaskr.py 파일을 다음의 내용으로 만든다.

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)

import 다음에는 2줄을 띄우는 게 관례인 것 같다. PEP 8에는 최상위 함수와 클래스 정의는 2줄을 띄운다고 나와 있는데 최상위 함수의 기분이 좀 헷갈리지만 일단 import 구문과 코드를 구분하는데 2줄을 띄우는 정도는 이해했다. 난 2줄을 띄우는 습관이 없어서 아직 엄청 어색하지만... 관례가 익숙지 않으므로 flake8 같은 도구를 사용해서 작성한 코드를 계속 검색할 수 있게 해야할텐데 이 방법은 이후 더 찾아봐야 할 것 같다.

app = Flask(__name__)는 주석 설명대로 Flask 인스턴스를 생성한 것이다. python에서 __name__은 모듈의 이름을 뜻한다. 모듈의 이름이란 게 애매한 의미로 느껴지는 데 테스트해보면 파일명이 바로 모듈의 이름이 되는 것으로 보인다. 그래서 이 튜토리얼은 flaskr를 만들기 때문에 시작 파일이 flaskr.py가 되는 것 같다. 여기서 모듈 이름이란 의미는 flaskr.pyimport해서 사용하면 __name__의 값이 flaskr이 된다. 추가로 python flaskr.py처럼 직접 실행할 때는 __name__의 값이 __main__가 된다. 파이썬 코드에서 많이 본 if __name__ == "__main__":import해서 사용하는 경우인지 직접 실행한 경우인지를 구분하기 위한 것이다.

그다음은 app.config.update()에 딕션어리를 넣고 있다. 코드를 보면 데이터베이스 관련 설정값을 추가하는 것으로 충분히 예측할 수 있다. app.conf를 출력해 보면 다음과 같이 출력된다.

<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}>

지금 당장은 각 값의 사용 용도는 알 수 없지만 대충 예상할 수는 있다. 앞의 코드처럼 딕션어리로 업데이트한 뒤의 값을 보면 SECRET_KEY처럼 이미 있던 키는 갱신되고 새로운 키는 추가되는 것을 확인할 수 있다.

app.config를 업데이트하는 값을 보면 DATABASE=os.path.join(app.root_path, 'flaskr.db')부분이 있다. os.path는 경로와 관련된 모듈이다. os.path.join()은 전달한 값을 연결한 경로로 반환한다. app.root_pathFlask 애플리케이션 객체에 있는 속성인데 문서를 보면 자동으로 애플리케이션의 루트 경로로 잡힌다. 그래서 flaskr/flaskr/flaskr.py 파일이 있는 /Users/outsider/flaskr/flaskr/가 이 값이 되고 flaskr.db와 연결했으므로 최종적으로 /Users/outsider/flaskr/flaskr/flaskr.db가 된다.

인스턴스 폴더

튜토리얼의 설명을 보면 OS가 프로세스의 워킹 디렉터리를 할 수 있지만 보통 한 프로세스에서 여러 애플리케이션을 띄울 수 있으므로 app.root_path를 사용한다고 나와 있고 데모가 아닌 실제 애플리케이션에서는 Instance Folder를 추천한다고 나와 있다. 그래서 인스턴스 폴더가 궁금해졌다.

인스턴스 폴더는 Flask 애플리케이션을 생성할 때 app = Flask(__name__, instance_path='/path/to/instance/folder')처럼 지정할 수 있다. 실제로 어떤 상황에 유용한지는 Flask를 더 써봐야겠지만 문서를 보면 app.root_path는 패키징되지 않은 경우에만 잘 동작한다고 되어 있다. 모듈로 PyPI에 올리는 경우가 아닌 Flask처럼 웹 애플리케이션을 작성하는 상황에서 Python에서 말하는 패키징이라는 개념을 아직 제대로 이해하지 못했지만, Flask 애플리케이션을 다른 곳에서 불러다 사용하는 경우 root_path는 패키지의 루트 경로가 될 것이므로 불러올 구성 파일 같은 경우도 패키지에 포함되어야 한다. 이때 instance_path를 다른 경로의 파일을 참조하도록 할 수 있고 패키지에 포함하지 않아도 되므로 시크릿 정보가 있어서 설정 파일을 다른 곳에 두거나 외부에서 구성파일을 받고자 하는 경우 사용하는 것으로 이해했다. 아니면 서버 배포를 하는 경우도 스테이지나 프로덕션 서버별로 파일을 다르게 지정해서 배포할 수 있으니 이때도 사용할 수 있다.

지정하지 않으면 루트 경로의 /instance가 값이 되고 설치한 모듈이나 패키지일 때 /path/to/site-packages/myapp 등이 된다. 지정할 때는 반드시 절대 경로로 지정해야 하고 상대경로를 사용해야 하는 경우는 app = Flask(__name__, instance_relative_config=True)처럼 설정해 주어야 한다.

설정 파일 분리

튜토리얼에서는 flaskr.py 파일에 직접 설정하고 있지만, 보통은 별도의 .ini.py 파일로 분리한 뒤 가져와서 사용하는 것을 추천한다. 보통 이 파일은 외부에서 제어할 수 있는 게 좋으므로 튜토리얼에서도 환경변수에서 가져오는 방법도 설명하고 있다. app.config.from_envvar('FLASKR_SETTINGS', silent=True)를 사용하면 FLASKR_SETTINGS라는 환경변수의 경로에 있는 파일에서 값을 가져와서 Config 객체에 설정할 수 있다. 여기서 파일의 종류가 문서에 명확하게 나와 있지 않아서 헷갈리는데 찾아보니 보통은 cfg 파일을 사용하는 것 같다.

그래서 위와 똑같은 설정을 하려면 아래와 같이 config.cfg를 만들고

DATABASE='/Users/outsider/flaskr/flaskr/flaskr.db'
SECRET_KEY='development key'
USERNAME='admin'
PASSWORD='default'

FLASKR_SETTINGS=/Users/outsider/flaskr/config.cfg 환경 변수를 지정하면 Config 객체에 위의 내용이 들어가게 된다. 이때 silent=True를 하면 해당 환경변수가 없어도 오류가 나지 않는다.

JSON 파일을 사용하고 싶다면 app.config.from_json(os.environ['FLASKR_SETTINGS'], silent=True)처럼 사용할 수 있다. 이 외에도 파이썬 객체에서 가져오는 from_object 등 여러 가지가 있어서 필요 한대로 설정 파일을 가져와서 사용할 수 있다.

데이터베이스 접속

이번 튜토리얼의 마지막으로 flaskr/flaskr/flaskr.py 파일에 데이터베이스(여기서는 SQLite3) 접속에 사용한 함수를 추가한다.

def connect_db():
    """Connects to the specific database."""
    rv = sqlite3.connect(app.config['DATABASE'])
    rv.row_factory = sqlite3.Row
    return rv

튜토리얼에 아직 이 부분을 사용하는 코드가 나오지 않아서 정확한 위치를 모르겠는데 그냥 flaskr.py에 추가했다.

2017/11/12 22:32 2017/11/12 22:32

play.node 2017에서 발표한 "Node.js API 서버 성능 개선기"

Play.node 2017에서 발표를 했다. play.node 1회인 2012년에 발표한 뒤에는 운영진으로 참가했었고 운영진을 하면서 발표하는 건 보통 어려운 게 아니라는 걸 알게 되고는 이번에는 발표하기로 결정하고 운영진에서는 물러났다.

그동안 회사에서나 스터디에서 가끔 발표를 하다 보니 못 느끼고 있었는데 막상 발표하려고 했더니 공개적인 자리에서 발표하는 건 1년 만이라는 걸 깨달았다. 너무 오랜만이란 걸 알고 나니 약간 긴장은 됐지만, 발표가 처음은 아니라 그럭저럭 진행했다. 평소에는 훨씬 더 발표에 얘기할 내용이랑 시간 체크를 확실히 하는 편이지만 이번에는 발표 자료 준비에 큰 노력을 해서인지 피곤함에 발표 연습을 많이 못해서(실제로 자료도 당일 완성되었다.) 2분 먼저 시작했음에도 7분이나 초과하고 말았다. ㅠ 웬만해서는 발표시간을 맞추는 편인데... ㅠㅠ

발표해야겠다고 결정했을 때부터 하고 싶은 내용은 결정되어 있었다. Node.js 6.x와 8.x에 대해서 비교하고 싶었고 그 성능개선 과정에 관해서 얘기하고 싶었다. 관련해서 작년에 고생했던 Heap Dump 분석도 같이 얘기하면 괜찮을 것 같았다.

하지만 리얼월드 프로덕트는 항상 발표에 좋게 나오는 것은 아니므로 준비를 하면서 고생을 많이 했다. 테스트 환경이라 서버를 구성하면서도 큰 노력을 들였지만, 그 이후에도 한 번 실행하면 40분씩 걸리는 테스트를 돌려놓고 기다린 다음에 결과가 의도와 다르면 원인을 찾고 수정을 하고 다시 돌리고 다시 돌리고... 다시 돌리고... 지루함과 어려움의 연속이었지만 성능을 향상했다는 것이 목적이 아니라 성능 향상을 위한 결과 분석을 어떻게 하느냐는 게 발표의 목적이었기 때문에 발표를 위한 자료는 어느 정도 준비되었다.

처음에는 Node.js 6.x와 8.x의 결과가 나와서 이제 거의 다 해결된 것처럼 느껴졌지만 설명하기 좋은 CPU 프로파일링 결과 분석과 Heap Dump 의 내용까지도 정리하기까지는 시간이 꽤 걸렸다. 내 코드의 아주 명확한 병목이나 잘못된 부분이 나왔다면 발표는 좀 더 쉬웠겠지만, 코드가 괜찮다고(?) 안심해야 하는 건지 발표준비에 대한 걱정을 해야 하는 건지 심란한 시간을 보내면서 준비를 했다.

발표자료의 꾸미기에 좀 더 힘을 쓰는 편이긴 한데 이번에는 테스트하고 자료를 준비하느라고 꾸미기는 좀 생략한 게 좀 아쉬웠다. 생각보다 너무 많은 사람이 와서 약간의 긴장을 가지고 시작했고 보통 발표가 그렇듯이 평가가 어떤지는 몰라서 걱정에 휩싸였지만, 친분이 있어서 그런지 다들 좋은 피드백을 주셔서 좀 맘이 편안해졌다.

2017/11/10 00:42 2017/11/10 00:42