Outsider's Dev Story

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

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

Flask 공식 튜토리얼 따라하기 #2에 이어진 글이다.


4 단계: 데이터베이스 연결

앞에서 connect_db로 데이터베이스 연결을 구성하는 함수를 만들었지만 유용하지는 않다. 왜냐하면, 매번 데이터베이스를 연결하고 종료하는 것은 아주 비효율적이라서 연결을 더 오래 유지할 필요가 있다. 데이터베이스 연결은 하나의 트랜잭션을 가지므로 한 요청이 한 번에 하나의 연결을 사용함을 보장해야 한다. 이렇게 하는 좋은 방법은 애플리케이션 컨텍스트를 유틸라이징하는 것이다.

Flask에는 애플리케이션 컨텍스트와 리퀘스트 컨텍스트가 있지만 여기서는 이 컨텍스트를 사용할 특수 변수가 있다는 거만 알면 된다고 한다. request 변수는 현재 요청에 관한 요청 객체이고 g는 현재의 애플리케이션 컨텍스트와 연관된 범용 변수이다. 그냥 변수가 있는 거구나 싶지만, 뒤에서 더 살펴본다고 하니까 넘어가자.

지금은 g 객체에 정보를 안전하게 저장할 수 있다는 것을 알면 된다고 한다. 애플리케이션 컨텍스트의 범위를 정확히 몰라서 여기에 어떤 정보를 담아야 안전한지 잘 모르겠지만 이것도 일단 넘어갔다. 이 변수에 헬퍼 함수를 만들어서 처음 호출되었을 때 현재 컨텍스트에 데이터베이스 연결을 만들고 이후의 호출에서는 이미 생성한 연결을 반환해서 사용하겠다고 한다. 이 함수가 get_db()이고 나는 flaskr/flaskr.py에 이 함수를 넣었다.

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

이 함수로 디비에 접속할 수 있게 되었다. 반대인 접속해제는 Flask에서 teardown_appcontext() 데코레이터를 제공하고 있어서 애플리케이션 컨텍스트가 내려갈 때마다 실행된다고 한다. 이는 아래와 같이 선언할 수 있다.

@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()

튜토리얼 설명에 따르면 애플리케이션 컨텍스트는 요청이 들어오기 전에 만들어지고 요청이 끝날 때마다 제거된다고 한다. 애플리케이션 컨텍스트라는 이름도 그렇고 데이터베이스 접속을 끊는 과정을 여기에 넣어서 당연히 여기서 teardown은 앱이 내려갈 때를 의미하는 중 알았는데 요청마다 실행된다고 해서 실제로 로그를 출력해보니 정말 요청마다 teardown이 실행되었다.

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 -

자세한 내용은 Application Context 문서를 살펴보라고 한다.

애플리케이션 컨텍스트

Application Context를 살펴보라니까 자세히 좀 보기로 했다.

코드를 실행할 때 두 가지 상태가 존재하는 것이 Flask 설계 개념 중 하나다. 한 상태는 애플리케이션 설정 상태(application setup state)인데 Flask 객체가 인스턴스화 될 때 시작되고 첫 요청이 들어오면 자연히 종료되는데 이 상태에서는 다음 내용이 참이라고 가정한다.

  • 프로그래머는 애플리케이션 객체를 안전하게 수정할 수 있다.
  • 아직 요청을 처리하지 않았다.
  • 애플리케이션 객체를 수정하려면 참조해야 하는데 이 객체의 참조를 제공하는 다른 특별한 프락시는 존재하지 않는다.

반면 요청을 처리하는 중에는 다음 내용을 참이라고 가정할 수 있다고 하는데 이 요청을 처리하는 것을 또 하나의 상태로 정의하는 것으로 보인다.

  • 요청 처리 중에는 컨텍스트 로컬 객체(context local object, flask.request 등)가 현재 요청을 가리키고 있다.
  • 언제 어떤 코드든 간에 이 객체를 가질 수 있다.

이 외에 세 번째 상태도 있는데 요청을 처리하는 중 애플리케이션과 상호작용하는 방법과 비슷하게 요청이 없는 상태에서도 애플리케이션을 다루는 경우가 있다. 인터렉티브 Python 셸에서 애플리케이션과 상호작용 하는 명령행 애플리케이션을 예로 들 수 있다. 애플리케이션 컨텍스트가 current_app 컨텍스트 로컬을 강력하게 만드는 것이다. 이 부분은 나중에 앱을 작성하다 보면 제대로 이해할 수 있을 것 같다.

애플리케이션 컨텍스트의 목적

애플리케이션 컨텍스트가 존재하게 된 주요 이유는 과거 요청 컨텍스트에 여러 기능을 추가하려는데 더 나은 해결책이 없었기 때문이다. 이는 한 Python 프로세스에서 여러 애플리케이션을 사용할 수 있다는 Flask의 설계 원칙 중 하나 때문인데 이 상황에서 코드가 "올바른" 애플리케이션을 찾는 방법이 문제였다. 예전에는 애플리케이션을 명시적으로 전달하기를 권했는데 이를 고려하지 않고 만든 라이브러리에서 문제가 생겨서 현재 요청의 애플리케이션 참조에 바인딩 된 current_app 프락시를 나중에 사용하게 하는 것이 일반적인 해결책이었다고 한다.

애플리케이션 컨텍스트 생성

애플리케이션 컨텍스트를 생성하는 데는 두 가지 방법이 있다. 첫 번째 방법은 암묵적인 방법인데 요청 컨텍스트가 들어올 때마다 필요하다면 애플리케이션 컨텍스트도 생성한다. 그래서 필요하지 않다면 애플리케이션 컨텍스트의 존재는 무시해도 된다. app_context() 메서드를 명시적으로 사용하는 것이 두 번째 방법이다.

from flask import Flask, current_app

app = Flask(__name__)
with app.app_context():
    # 이 블록내에서는 current_app는 앱을 가리킨다.
    print current_app.name

SERVER_NAME이 설정된 경우 애플리케이션 컨텍스트는 url_for() 함수에서도 사용되므로 요청이 없더라도 URL을 생성할 수 있다. 요청 컨텍스트가 들어오지 않고 애플리케이션 컨텍스트를 명시적으로 설정하지 않았다면 RuntimeError가 발생할 것이다.

RuntimeError: Working outside of application context.


컨텍스트의 범위

애플리케이션 컨텍스트는 필요에 따라 생성되고 제거되며 스레드 간에 이동하지 않고 요청 간에 공유되지도 않는다. 그래서 데이터베이스 연결 정보 등을 저장하기에 안성맞춤이다. 내부 스택 객체를 flask._app_ctx_stack라고 부른다.

확장 기능도 최상위 수준에 추가적인 정보를 맘껏 저장할 수 있는데 이때 사용자 코드가 예약한 flask.g 객체 대신 충분히 구별되는 이름을 사용해서 정보를 저장한다고 가정한다. 더 자세한 내용은 Flask 확장 개발을 참고하라고 한다.

컨텍스트 사용 방법

요청당 생성되어야 하는 리소스를 캐시할 때 보통 컨텍스트를 사용하는데 데이터베이스 연결이 대표적이다. 컨텍스트가 Flask 애플리케이션과 확장 간에 공유되므로 애플리케이션 컨텍스트에 저장할 때 유일한 이름을 선택하는 것이 필수다. 가장 일반적인 사용방법은 리소스 관리를 두 가지로 나누는 것이다.

  1. 컨텍스트의 캐싱된 리소스
  2. 리소스 할당 해제에 기반을 둔 컨텍스트 제거

보통 존재하지 않으면 리소스 X를 생성하고 존재하면 같은 리소스를 반환하는 get_X() 함수와 teardown 핸들러로 등록된 teardown_X() 함수가 있을 수 있다.

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()

처음 get_db()가 호출되면 연결이 이뤄지고 암묵적으로 이를 사용하려고 LocalProxy를 사용할 수 있다.

from werkzeug.local import LocalProxy
db = LocalProxy(get_db)

이 방법을 이용하면 내부적으로 get_db()를 호출하면서 데이터베이스에 직접 접근할 수 있다.

2018/02/12 02:53 2018/02/12 02:53