앱·요청 컨텍스트 — current_app·request·g·session이 동작하는 방식

앱·요청 컨텍스트 — current_app·request·g·session이 동작하는 방식

Flask에서 current_app, g, request, session 같은 것들이 어디서 오는 건지 궁금했죠? 이들은 요청·CLI 명령 같은 활동 중에 데이터와 객체를 추적하는 컨텍스트(context) 덕분에 동작해요. 이 데이터를 모든 함수에 인자로 넘기는 대신 프록시(proxy)로 접근하는 방식이에요.

요청을 처리할 때는 요청 데이터와 애플리케이션 데이터를 함께 담고 있어서 요청 컨텍스트 라고 불러요. CLI 명령처럼 다른 경우엔 앱 컨텍스트라고 해요. 앱 컨텍스트에서는 :data:.current_app:data:.g를 쓸 수 있고, 요청 컨텍스트에서는 거기에 더해 :data:.request:data:.session도 쓸 수 있어요.

출처: 공식문서 - The App and Request Context

본문

컨텍스트의 목적

컨텍스트와 프록시는 개발 중 두 가지 문제를 해결해 줘요. 순환 임포트(circular imports)와, 요청 중 전역 데이터를 넘겨주는 문제예요.

:class:.Flask 애플리케이션 객체에는 :attr:~.Flask.config 같은 속성이 있는데, 뷰와 다른 함수 안에서 접근하면 유용해요. 하지만 프로젝트의 모듈 안에서 app 인스턴스를 임포트하는 건 순환 임포트 문제에 빠지기 쉬워요. 앱 팩토리 패턴 을 쓰거나 재사용 가능한 블루프린트확장을 작성할 땐 임포트할 app 인스턴스가 아예 없을 수도 있어요.

애플리케이션이 요청을 처리하면 :class:.Request 객체를 만들어요. 한 *워커(worker)*는 한 번에 하나의 요청만 처리하므로, 그 요청 중에는 요청 데이터를 그 워커에 전역으로 볼 수 있어요. 이를 요청 동안 모든 함수에 인자로 넘기는 건 장황하고 중복적이 돼요.

Flask는 이 문제들을 활성 컨텍스트(active context) 패턴으로 해결해요. app을 직접 임포트하거나 그것과 요청을 모든 함수에 넘기는 대신, 현재 활성화된 애플리케이션과 요청 데이터를 가리키는 프록시를 임포트하고 접근해요. 이를 "컨텍스트 로컬(context local)" 데이터라고 부르기도 해요.

설정 중 컨텍스트

앱 컨텍스트 밖에서 :data:.current_app, :data:.g 또는 그것을 쓰는 뭔가에 접근하려 하면 이런 오류 메시지를 보게 돼요.

RuntimeError: Working outside of application context.

Attempted to use functionality that expected a current application to be
set. To solve this, set up an app context using 'with app.app_context()'.
See the documentation on app context for more information.

애플리케이션을 설정하는 중(예: 확장을 초기화할 때) 그 오류가 보인다면, app에 직접 접근할 수 있으니 컨텍스트를 수동으로 push하면 돼요. :meth:.Flask.app_contextwith 블록에서 써요.

def create_app():
    app = Flask(__name__)

    with app.app_context():
        init_db()

    return app

애플리케이션 설정과 상관없는 코드의 다른 곳에서 그 오류가 보인다면, 그 코드를 뷰 함수나 CLI 명령으로 옮겨야 한다는 신호일 가능성이 높아요.

테스트 중 컨텍스트

테스트 중 컨텍스트를 관리하는 자세한 내용은 테스팅 문서 를 봐요.

요청 컨텍스트 밖에서 :data:.request, :data:.session 또는 그것을 쓰는 뭔가에 접근하려 하면 이런 오류 메시지를 보게 돼요.

RuntimeError: Working outside of request context.

Attempted to use functionality that expected an active HTTP request. See the
documentation on request context for more information.

이건 아마 테스트 중에만 일어나요. 테스트와 상관없는 코드의 다른 곳에서 그 오류를 본다면 그 코드를 뷰 함수로 옮겨야 한다는 신호일 가능성이 높아요.

이 문제를 푸는 주된 방법은 :meth:.Flask.test_client로 완전한 요청을 시뮬레이션하는 거예요.

전체 요청이 아니라 함수 하나만 단위 테스트하고 싶다면 :meth:.Flask.test_request_contextwith 블록에서 써요.

def generate_report(year):
    format = request.args.get("format")
    ...

with app.test_request_context(
    "/make_report/2017", query_string={"format": "short"}
):
    generate_report()

컨텍스트의 가시성

컨텍스트는 요청, CLI 명령, with 블록 같은 활동과 같은 수명을 가져요. 앱에 등록된 다양한 콜백과 시그널이 컨텍스트 동안 실행돼요.

Flask 애플리케이션이 요청을 처리하면 요청 컨텍스트를 push해서 활성 애플리케이션과 요청 데이터를 설정해요. CLI 명령을 처리하면 앱 컨텍스트를 push해서 활성 애플리케이션을 설정해요. 활동이 끝나면 그 컨텍스트를 pop해요. :data:.request, :data:.session, :data:.g, :data:.current_app 같은 프록시 객체는 컨텍스트가 push되어 활성 상태인 동안 접근할 수 있고, pop된 뒤에는 접근할 수 없어요.

컨텍스트는 각 스레드(또는 다른 워커 타입)에 고유해요. 프록시는 다른 워커로 전달할 수 없어요. 다른 워커는 다른 컨텍스트 공간을 가지며 부모 공간의 활성 컨텍스트를 알지 못하거든요.

각 워커로 범위가 한정되는 것 외에도, 프록시 객체는 프록시된 실제 객체와 별개의 타입과 정체성을 가져요. 어떤 경우엔 프록시가 아니라 실제 객체에 접근해야 해요. 그런 경우 :meth:~.LocalProxy._get_current_object 메서드를 쓰세요.

app = current_app._get_current_object()
my_signal.send(app)

컨텍스트의 수명주기

Flask는 여러 단계로 요청을 디스패치하며, 이는 요청·응답·오류 처리 방식에 영향을 줄 수 있어요. 각 요청 동안의 모든 단계·콜백·시그널 목록은 수명주기 문서 를 봐요. 다음은 컨텍스트와 직접 관련된 단계들이에요.

  • 앱 컨텍스트가 push되어 프록시를 쓸 수 있어요.
  • :data:.appcontext_pushed 시그널이 전송돼요.
  • 요청이 디스패치돼요.
  • :meth:.Flask.teardown_request로 장식된 함수들이 호출돼요.
  • :data:.request_tearing_down 시그널이 전송돼요.
  • :meth:.Flask.teardown_appcontext로 장식된 함수들이 호출돼요.
  • :data:.appcontext_tearing_down 시그널이 전송돼요.
  • 앱 컨텍스트가 pop되어 프록시를 더 이상 쓸 수 없어요.
  • :data:.appcontext_popped 시그널이 전송돼요.

teardown 콜백은 컨텍스트가 pop될 때 호출돼요. 디스패치 중 처리되지 않은 예외가 있어도 호출돼요. 일부 테스트 시나리오에서 여러 번 호출될 수 있어요. 즉 요청 디스패치의 다른 부분이 실행됐다는 보장이 없다는 뜻이에요. 이 함수들은 다른 콜백에 의존하지 않게 작성해야 해요. 어떤 콜백이 오류를 던져도 모든 콜백이 호출돼요.

컨텍스트가 동작하는 방식

컨텍스트 로컬은 파이썬의 :mod:contextvars와 Werkzeug의 :class:~werkzeug.local.LocalProxy로 구현돼요. 파이썬의 contextvars는 스레드나 코루틴에 로컬한 데이터를 관리하는 저수준 구조예요. LocalProxy는 contextvar를 감싸서 속성·메서드 접근이 contextvar에 저장된 객체로 전달되게 해요.

컨텍스트는 스택처럼 추적되며, 활성 컨텍스트가 스택 맨 위에 있어요. Flask는 요청, CLI 명령, 테스팅, with 블록 등에서 컨텍스트의 push와 pop을 관리해요. 프록시는 활성 컨텍스트의 속성에 접근해요.

스택이기 때문에 이미 활성인 컨텍스트 동안 다른 컨텍스트를 push해서 프록시를 바꿀 수도 있어요. 흔한 패턴은 아니지만 고급 사용 사례에서 쓸 수 있어요. 예를 들어 Flask 애플리케이션을 WSGI 미들웨어로 쓰고 뷰에서 다른 Flask 앱을 감싸서 호출할 수 있어요.

더 알아보기