Django Signals 기초

고봉진·2023년 6월 15일
0

Django 공식 문서(4.2v)를 정리한 글입니다.

Signals 기초

어떤 동작(action)이 실행되었을 때, 지정된 발신자(senders)에서 수신자(receivers)에게 동작이 실행되었음을 알려주는 기능. 여러 개의 다른 코드가 한 이벤트에 의해 촉발되어야 할 때 특히 유용하다.

from django.apps import AppConfig
from django.core.signals import setting_changed


def my_callback(sender, **kwargs):
    print("Setting changed!")


class MyAppConfig(AppConfig):
    ...

    def ready(self):
        setting_changed.connect(my_callback)

AppConfig.read에서 setting_changed.connect 메서드로 my_callback 함수를 연결한다. 레지스트리가 채워졌을 때(서버가 구동 시작을 마치고 어플리케이션이 완전히 로드되었을 때) 연결된 my_callback 함수를 호출한다.

from django.apps import AppConfig
from django.db.models.signals import pre_save


class RockNRollConfig(AppConfig):
    # ...

    def ready(self):
        # importing model classes
        from .models import MyModel  # or...

        MyModel = self.get_model("MyModel")

        # registering signals with the model's string label
        pre_save.connect(receiver, sender="app_label.MyModel")

[!Warning]
콤포넌트의 변경이 다른 콤포넌트들의 변경을 요구하는 내부 의존성을 줄이기 위한 느슨한 결합(Loose Coupling)의 모양을 갖게 하지만 코드가 이해, 변경, 디버그 하기 어려워질 수 있기 때문에, 가능하다면 시그널을 사용하는 대신 해당 메서드 내에서 다른 메서드를 직접 호출할 것.

시그널 듣기

수신자 함수를 Signal.connect() 메서드를 사용해 등록한다. 시그널이 발생할 때 수신자 함수가 등록된 순서대로 호출된다.

Signal.connect(receiver, sender=None, weak=True, dispatch_uid=None)[source]

Parameters
  • receiver : 시그널이 발생할 때 호출될 함수.
  • sender : 어떤 Model에서 발생하는 시그널을 사용할 것인가?
  • weak : 인스턴스가 생성되어 유지되고 있는 메서드가 아닌 로컬 함수를 수신자 함수로 지정했을 때, 가비지 콜렉트 당할 수 있는데, 이것을 방지하기 위해 weak 인자에 False를 전달하라.
  • dispatch_uid : 시그널 중복을 방지하기 위해 고유 값을 전달.

수신자 함수

def my_callback(sender, **kwargs):
    print("Request finished!")

모든 수신자 함수는 senderkwargs를 인자로 받아야 한다. request_finishedsender 이외에 다른 인수를 전달하지 않기 때문이다.

그렇다고 해서 my_callback(sender)라고만 하면 Django는 에러를 발생시킨다. 어느 시점에 다른 인자가 추가될 수 있기 때문이라고 한다.

수신자 함수 연결하기

두가지 방법이 있다.

직접 연결하기:

from django.core.signals import request_finished

request_finished.connect(my_callback)

receiver(signal, **kwargs)데코레이터 사용

from django.core.signals import request_finished
from django.dispatch import receiver


@receiver(request_finished)
def my_callback(sender, **kwargs):
    print("Request finished!")

인수로 신호 인스턴스를 전달한다. request_finished에 의해 매 요청이 완료되면 신호가 발생해 my_callback 함수를 호출하도록 되어있다.

다음은 receiver 데코레이터의 소스코드이다:

def receiver(signal, **kwargs):
    """
    A decorator for connecting receivers to signals. Used by passing in the
    signal (or list of signals) and keyword arguments to connect::

        @receiver(post_save, sender=MyModel)
        def signal_receiver(sender, **kwargs):
            ...

        @receiver([post_save, post_delete], sender=MyModel)
        def signals_receiver(sender, **kwargs):
            ...
    """

    def _decorator(func):
        if isinstance(signal, (list, tuple)):
            for s in signal:
                s.connect(func, **kwargs)
        else:
            signal.connect(func, **kwargs)
        return func

    return _decorator

주석에서 볼 수 있는 것 처럼 여러가지 신호에 반응하게 할 수 있다.

📝 시그널을 다루는 코드와 등록하는 코드는 어디든 존재할 수 있지만, 임포트시 발생하는 부작용을 최소화하기 위해 어플리케이션의 root 모듈이나 models 모듈에 둘 것을 권장한다.
In practice, 시그널 핸들러(시그널이 발생할 때 실행되는 콜백 함수)는 주로 어플리케이션의 signals 서브모듈에 위치시키고, @receiver 데코레이터를 사용해 만들어진 시그널 리시버는 어플리케이션 configuration class의 ready 메서드 안에서 연결된다(apps.py). receiver 데코레이터를 사용하는 경우 아래와 같이 signals 서브모듈을 임포트하면 암시적으로 시그널 핸들러와 연결된다.

apps.py

from django.apps import AppConfig
from django.core.signals import request_finished


class MyAppConfig(AppConfig):
    ...

    def ready(self):
        # Implicitly connect signal handlers decorated with @receiver.
        from . import signals

        # Explicitly connect a signal handler.
        request_finished.connect(signals.my_callback)

예를 들어, 아래와 같이 Post, Review가 삭제되었을 때 실행될 시그널 핸들러들을 signals.py에 배치시킨다.

# signals.py
from django.dispatch import receiver
from django.core.files.storage import default_storage
from django.db.models.signals import pre_delete

from .models import Post, Review


@receiver(pre_delete, sender=Post)
def delete_post_images(sender, instance, **kwargs):
    instance.review_set.all().delete()

    for post_image in instance.post_images.all():
        default_storage.delete(post_image.image.name)
        post_image.delete()


@receiver(pre_delete, sender=Review)
def delete_reivew_images(sender, instance, **kwargs):
    for review_image in instance.review_images.all():
        default_storage.delete(review_image.image.name)
        review_image.delete()

각 모델에서 발생하는 pre_delete 시그널을 받아 실행된다. connect 메서드로 연결되지 않았지만 해당 핸들러들을 실행시킬 수 있는 이유는, models.Modeldelete 메서드에서 Collector 클래스의 delete 메서드를 실행하는데, 아래에서 볼 수 있듯, 여기서 pre_delete.send 메서드로 신호를 보내기 때문이다.

pre_delete 메서드는 ModelSignal 클래스의 인스턴스이다.

# django.db.models.deletion
class Collection:
	...
	
	def delete(self):
		...
	
	     with transaction.atomic(using=self.using, savepoint=False):
	        # send pre_delete signals
	        for model, obj in self.instances_with_model():
	            if not model._meta.auto_created:
	                signals.pre_delete.send(
	                    sender=model,
	                    instance=obj,
	                    using=self.using,
	                    origin=self.origin,
	                )
		
		...
    
    ...
    

즉, 시그널 핸들러 함수를 호출하려면 우선 connect로 등록 후 send로 신호를 보내거나, receiver 데코레이터를 사용해야 한다.

from django.db import models
from django.dispatch import receiver

from .signals import demo_signal


class Demo(models.Model):
    demo = models.CharField("demo", max_length=50)

    def send_signal(self):
        demo_signal.send()
        print('signal sent')

    def connect_receiver(self):
        demo_signal.connect(signal_handler, sender=self)


@receiver(demo_signal)
def signal_handler(**kwargs):   # 반드시 **kwargs를 인자로 받아야 한다.
    print('signal handled')

📝 또, AppConfig.ready 메서드는 테스트시 여러 번 호출될 수 있기 때문에, dispatch_uid를 사용해 신호 중복을 방지하는게 좋다.

특정 발신자 지정하기

어떤 모델 인스턴스가 저장 또는 삭제되었을 때, 그 특정 모델이 발생시키는 신호만 수신하도록 아래와 같이 sender 인자에 모델을 전달할 수 있다

from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import MyModel


@receiver(pre_save, sender=MyModel)
def my_handler(sender, **kwargs):
    ...

pre_save 시그널(메서드)은 어떤 인스턴스가 저장되려 할 때, 즉 save 메서드가 호출되는 시점, 하지만 아직 저장되지는 않은 시점에 발생해 리시버를 통해 등록된 시그널 핸들러를 호출한다. sender, instance, raw, using, update_fields를 인자로 받는다. 여기서 MyModel 클래스를 sender로 전달하고 있고, 위 receiver 데코레이터를 통해 pre_save 메서드에 전달한다. 해당 클래스 인스턴스의 save 메서드가 실행될 때 신호가 발생한다.

위 예를 다시 보자.

from django.db import models
from django.dispatch import receiver
from django.db.models.signals import pre_delete

from .signals import demo_signal

# Create your models here.
class Demo(models.Model):
    demo = models.CharField("demo", max_length=50)

    def send_signal(self):
        demo_signal.send(sender=self.__class__)
        print('signal sent')

class Dummy(models.Model):
    dummy = models.CharField('dummy', max_length=50)

    def send_signal(self):
        demo_signal.send(sender=self.__class__)
        print('dummy signal sent')


@receiver(demo_signal, sender=Demo)
def signal_handler(**kwargs):
    print('demo signal handled')

@receiver(demo_signal, sender=Dummy)
def signal_handler(**kwargs):
    print('dummy signal handled')
    

위와 같이 각 핸들러 함수에 sender 모델을 전달하고, send 메서드에, 각 인스턴스의 클래스를 전달하면, 원하는 모델에서 원하는 핸들러를 호출할 수 있다.

In [1]: dummy = Dummy.objects.first()

In [2]: dummy.send_signal()
dummy signal handled
dummy signal sent

In [3]: demo = Demo.objects.first()

In [4]: demo.send_signal()
demo signal handled
signal sent

어떤 시그널이 어떤 sender를 받는지는 공식 문서를 참고하자.

시그널 중복 방지하기

특정 환경에서 수신자와 발신자를 연결하는 코드가 여러번 실행될 수 있다. 이 경우 신호가 발생될 때 시그널 핸들러 함수가 여러번 실행된다. 예를 들어 위에서 언급한 AppConfig.ready 메서드가 있다. 좀 더 일반적으로 말하면, 시그널이 정의된 모듈을 임포트 할 때마다 실행되는데, 임포트 되는 만큼 시그널 등록(signal registration) 코드가 실행되기 때문이다.

예를 들어, 시그널이 발생할 때마다 이메일을 발송하는 로직이 있다면 문제가 될 수 있다. 이를 방지하기 위해 dispatch_uid를 사용한다. 문자열 또는 hashable 객체면 충분하다. 여러번 실행되더라도 같은 dispatch_uid 값을 갖는 신호는 한번만 처리된다.

from django.core.signals import request_finished

request_finished.connect(my_callback, dispatch_uid="my_unique_identifier")

시그널 정의하기/보내기

시그널 커스텀하기

시그널 정의하기

모든 시그널은 django.dispatch.Signal의 인스턴스이다. [source]

import django.dispatch

pizza_done = django.dispatch.Signal()

이제 pizza_done을 시그널로 사용할 수 있다.

시그널 보내기

두 가지 방법이 있다.

  • Signal.send(sender, **kwargs) :
  • Signal.send_robust(sender, **kwargs) :

둘 다 receiver와 response 튜플로 이루어진 리스트를 반환한다([(receiver, response), ... ]).

send는 에러를 지나가게 둔다(propagate; pass on, transmit).
send_robust는 에러를 캐치 → 모든 수신자가 신호를 받았음을 확실히 한다. 에러가 발생하면 에러 인스턴스가 response로 반환된다.
traceback은 __traceback__ 속성에 있다.


연결 종료하기

Signal.disconnect(receiver=None, sender=None, dispatch_uid=None)
종료시 True를 반환하지만 sender가 lazy reference로 전달된 경우(<app label>.<model>) None을 반환한다. 연결시 dispatch_uid를 사용한 경우 None을 반환한다.



참고자료

profile
이토록 멋진 휴식!

0개의 댓글