파이썬 플라스크(Flask)에서 프레임워크 내부 동작을 감지하는 방법인 시그널(Signals)에 대해 살펴봅니다.

소개

플라스크는 내부적으로 템플릿을 렌더링한다거나 HTTP 요청을 준비하는 등 눈에 보이지 않는 곳에서 다양한 작업을 수행합니다. 일반적인 경우라면 신경쓸 필요는 없지만, 개발자가 플라스크의 내부 동작에 간섭해야 하는 경우가 있습니다:

  • 처리 과정 중 로그 기록
  • 사용자 인증 처리

이를 위해, 플라스크는 플라스크 내부의 코드 중간에 백엔드 웹 개발자가 정의한 함수를 실행할 수 있는 기능을 제공합니다. 그중 하나가 바로 시그널입니다. 특정 순간에 연결된 함수를 실행하는 것이 마치 신호를 발생시키는 것 같다고 하여, 영어로 신호라는 뜻을 가진 시그널이라는 이름이 붙게 되었습니다.

시그널 vs 내장 데커레이터

플라스크 내부 동작을 감지하는 방식으로는 시그널만 있는 것이 아닙니다. 플라스크는 내장 데커레이터라는 또다른 방식을 제공합니다.

내장 데커레이터로는 before_request(), after_request(), teardown_request() 등이 있습니다. 내장 데커레이터와 시그널 둘 다 플라스크의 내부 코드 흐름에 백엔드 웹 개발자가 만든 함수를 실행할 수 있도록 해주지만, 내장 데커레이터와 달리 시그널은 다음과 같은 특징을 가지므로 주의해야 합니다:

  • 시그널의 종류가 내장 데커레이터의 종류보다 더 다양합니다.
  • 시그널 처리 함수에서 데이터를 수정하는 것은 권장되지 않습니다. 데이터를 수정하고 싶다면 내장 데커레이터를 사용하세요1.
  • 여러 시그널 처리 함수가 연결되었을 때 특정 순서대로 함수가 실행되는 것을 보장하지 않습니다. 순서 보장을 원하는 경우 내장 데커레이터를 사용하세요1.

설치

시그널을 사용하기 위해서는 먼저 Blinker(블링커)라는 라이브러리를 설치해야 합니다. 용량 및 의존성 문제로 인해 이 라이브러리는 플라스크를 설치할 때 자동으로 함께 설치되지 않기 때문입니다.

Linux, macOS라면 pip install blinker를, Windows라면 py -m pip install blinker를 통해 Blinker를 설치합시다.

만약 Blinker를 설치하지 않고 시그널을 사용하려 하면 다음과 같은 오류가 발생합니다:

RuntimeError: Signalling support is unavailable because the blinker library is not installed.

시그널 객체

Blinker에서 신호를 주고받기 위해서는 시그널 객체가 필요합니다. Signal() 생성자를 통해 시그널 객체를 생성합니다:

from blinker import Signal

sig = Signal()

시그널 객체의 send() 메서드를 통해 신호를 보낼 수 있으며, connect() 메서드로 신호를 받는 함수를 연결할 수 있습니다.

send()의 첫 번째 인자인 sender로 누가 이 시그널을 일으켰는지에 대한 정보를 보낼 수 있습니다. 이 sender는 시그널 감지 함수의 첫 번째 매개변수로 들어오는데, 이를 통해 특정 sender인 경우에만 신호를 처리하도록 할 수 있습니다:

from blinker import Signal

sig = Signal()

def hi(sender):
    print("Hi", sender)

sig.connect(hi)
sig.send("John")
sig.send("Paul")

보통은 플라스크 내장 시그널만을 사용하기 때문에 우리가 직접 시그널 객체를 만들 일은 거의 없을 겁니다. 다만 플라스크의 기능을 확장하는 라이브러리를 만든다면 직접 시그널을 만들어 백엔드 웹 개발자에게 제공해야 할 수도 있습니다.

플라스크 내장 시그널

다음은 request_started 시그널을 통해 HTTP 요청이 발생했을 경우 "Request received!"를 출력하는 코드입니다 (플라스크 내장 시그널은 Flask 객체를 sender로 전송합니다):

# 1. 시그널 `import`
from flask import request_started 

# 2. 시그널을 처리할 함수 만들기
def when_request_started(sender):
    print("Request received! Sender:" sender)

# 3. 함수 연결
request_started.connect(when_request_started)
  1. 시그널 import

    먼저 request_started라는 시그널을 import로 가져옵니다. 이 시그널은 플라스크 내부에서 HTTP 요청이 처리되기 직전을 감지합니다. 이외에도 request_finished, template_rendered다양한 내장 시그널이 있습니다.

  2. 시그널을 처리할 함수 만들기

    시그널을 처리할 함수를 만듭니다. 이때 시그널을 발생시킨 주체인 Flask 객체가 sender라는 매개변수로 들어옵니다. 만약 여러 플라스크 애플리케이션 객체를 사용하는 특수한 경우라면, 이 sender를 통해 앱 별로 필터링할 수 있습니다.

  3. 함수 연결

    request_started 시그널의 connect() 메서드를 통해 시그널 처리 함수를 시그널과 연결합니다. 또는 @connect_via를 통해 데커레이터 형태로 함수를 등록할 수도 있습니다. 두 메서드의 실질적인 역할은 같습니다.

고급 기능

이름 있는 시그널

시그널 객체는 이름이 있는 경우와 없는 경우로 나뉘어집니다. 이름이 있는 경우 signal()이라는 함수를 호출합니다. 소문자로 시작함에 유의하세요. 이 함수는 내부적으로 NamedSignal이라는 객체를 만듭니다. 이름이 없는 경우 Signal()을 호출해 생성합니다:

from blinker import Signal, signal

named_ready = signal("named-ready")  # 이름 있는 시그널
anonymous_ready = Signal()  # 이름 없는 시그널

특정 sender만 처리

connect() 메서드에 sender 키워드 매개변수를 제공하면 특정 sender로부터 발생하는 신호만 처리하게 됩니다. 이를 통해 더 편리하게 특정 sender만 처리할 수 있습니다:

from blinker import Signal

sig = Signal()

def hi(sender):
    print("Hi", sender)

sig.connect(hi, sender="Paul")  # `"Paul"`만 처리
sig.send("John")  # 처리 안 함
sig.send("Paul")  # 처리

@connect_via 데커레이터

connect() 대신 @connect_via 데커레이터를 이용해 시그널 처리 함수를 손쉽게 등록할 수 있습니다:

@sig.connect_via("Paul")  # `"Paul"`만 처리
def hi(sender):
    print("Hi", sender)

참고

  1. https://flask.palletsprojects.com/en/1.1.x/signals/

    However, there are differences in how they work. The core before_request() handler, for example, is executed in a specific order and is able to abort the request early by returning a response. In contrast all signal handlers are executed in undefined order and do not modify any data.

     2