Django includes a “signal dispatcher” which helps decoupled applications get notified when actions occur elsewhere in the framework. In a nutshell, signals allow certain senders to notify a set of receivers that some action has taken place. They’re especially useful when many pieces of code may be interested in the same events.
분리된 app의 작업 발생을 알려고, 처리할 수 있도록 만들어진 기능
sender, signal, reciver의 형태로 되어 있으며, 특정 이벤트를 singnal이 포함하고 있으며, reciver가 sender로 부터 그 signal을 받는 것이다.
관찰자 모드, 게시-구독(Publish/Subscribe)이라고도 부른다.
# 우선 signals.py 를 특정 앱(여기선 user) 안에 만든다.
from django.db.models.signals import post_save
from django.contrib.auth.models import User
def create_profile(sender, instance, created, **kwargs):
if created == True:
user = instance
profile = Profile.objects.create(
owner = user,
user_name = user.username,
email = user.email,
name = user.first_name,
)
post_save.connect(create_profile, sender=User)
# 아래에서 더 자세히 보겠지만, connect를 쓰기 싫으면, 아래 app config를 진행하면 된다.
# signal의 코드 분리를 위해 app config에서, app이 load 될 때 signal을 import하게 한다.
# (user) apps.py
class UserConfig(AppConfig):
name = 'user'
def ready(self) -> None:
# DB signal
import app.user.signals
return super().ready()
위와 같이 작성하면, user가 save 되어서 created 될 때 마다 user가 sender로 써 reciver에게 instance(user model)과 어떤 signal인지 그 종류를 담아서 보낸다.
그래서 user가 생성됨에 따라 profile을 자동으로 만들게 할 수 있다. create 뿐 아니라 save이니 update, delete 등에 활용이 가능 하다. 그 signal을 조금 더 자세하게 살펴보자
그럼 어떻게 장고에서는 이런 이벤트를 발생하고, catch하게 만들어 두었을까? 우선 model단의 signal, core단의 signal의 전체 원리는 다르다. 하지만 모두 django.db.dispatch.dispatcher
에 있는 class Signal
를 뿌리로 하고 있다. signal에 대한 엄청난 deeeep dive는 여기서 확인이 가능하다. : Advanced Django Training
django.db.models > base.py
를 보면, 해당 signal을 모두 실질적으로 호출을 한다. 우리는 이렇게 core에 호출을 하는 signal에 대해 callback 함수를 만들어서 connect만 해주면 되는 것이다.
Signal코어를 더 살펴보면, signal class의 connect
가 핵심적으로 sender - reciver를 이어 주며, 데코레이터 세팅도 receiver
라는 함수로 세팅이 되어 있다. 해당 코어 함수를 살펴보면서 해당 글을 읽으면 더욱 더 좋다.
"생성자의 구분" 을 위해 models와 core 2가지로 분리를 해 두었고 그에 따라 signals에서 활용되는 함수는 파라미터값이 다르다.
Signal.connect(receiver, sender=None, weak=True, dispatch_uid=None)
를 조금 더 자세히 살펴보자.
django.db.models.signals.pre_init
django.db.models.signals.post_init
django.db.models.signals.pre_save
django.db.models.signals.post_save
django.db.models.signals.pre_delete
django.db.models.signals.post_delete
django.db.models.signals.m2m_changed
django.db.models.signals.class_prepared
connect
함수를 활용하여, 우리가 정의한 함수와 모델을 이어준다. 이게 기본으로 모든 종류의 시그널을 세팅하는 방법이다. 특정 Model이 __init__()
될 때, 즉 model class로 object (instance)를 만들 때 생성자가 호출이 된다. (당연하다, OOP인 python의 생성자가 init이다!)
q = Question(question_text="What's new?", pub_date=timezone.now())
의 경우 Question object(instance)가 만들어 졌다. 여기서 sender는 Question, args는 빈 리스트 (특정 모델의 __init__() 파라미터가 없으니까
), kwags는 {'question_text': "What's new?", 'pub_date': datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)}
와 같다.
kwargs는 특정 모델의 attributes 값이 전달이 되는 것이다.
위에서 말 했듯이 pre는 전, post는 후 의 시점 차이일 뿐 이지 모두 동일하다.
주의 할 점이 하나있다, 바로 returned during queryset iteration 대상으로 모두 init signal callback을 호출하기 때문에 query를 하지 말라고 한다! 아래는 가이드 원문이다.
you shouldn’t perform queries in receivers of pre_init or post_init signals because they would be executed for each instance returned during queryset iteration.
sender는 Model class, instance는 실제 당사자 모델 인스턴스, raw는 True로 DB에서 다른 레코드에 대한 쿼리를 날리거나 수정을 하지 못하게 한다. 아직 완벽하게 save가 끝난 상태가 아니기 때문이다!
using은 사용중인 db의 별명이고, update_fields 는 Model.save()에 전달된 업데이트 필드 집합 또는 None이다. None은 아무것도 전달되지 않았다는 의미다.
그리고 post_save는 'created'를 가지는데, save()는 알다시피 있으면 update, 없으면 create이다. 즉 save는 Upsert
를 한다. 그래서 이게 만들어 졌는지, 업데이트되었는지 구분하기 위해 던져주는 값이다. True면 created, False면 updated의 의미다.
엄밀히 말하자면 model class의 신호가 아니라 field의 신호기 때문에 조금 다르다.
m2m는 이름답게 N:M 관계를 위해 서로 다양하게 바뀐 경우를 생각한다. ManyToManyField 가 바뀌었을 때 보내는 signal이며 action
이라는 파라미터 값으로 "pre/post_add, pre/post_remove, pre/post_clear" 값을 받는게 핵심이다.
django.core.signals.pre_migrate
django.core.signals.post_migrate
이 migrate signal은 application이 install 되기 전(pre), 후(post)로 보내진다.
sender는 AppConfig instance 이다. app_config도 sender와 동일하다. 하지만 app_config를 제공하려면 특정 AppConfig 에서 ready()에 signal을 등록해야 한다!
verbosity는 manage.py cli를 쓰면서 option으로 주었던 값과 동일하다. "Indicates how much information manage.py is printing on screen", 자세한 설명은 공식 가이드 문서로 대체한다.
interactive는 원문으로 대체한다. If interactive is True, it’s safe to prompt the user to input things on the command line. If interactive is False, functions which listen for this signal should not try to prompt for anything.
For example, the django.contrib.auth app only prompts to create a superuser when interactive is True.
plan은 마이그레이션 plan에 대한 True, False 값을 명시한다. The migration plan that was used for the migration run. While the plan is not public API, this allows for the rare cases when it is necessary to know the plan. A plan is a list of two-tuples with the first item being the instance of a migration class and the second item showing if the migration was rolled back (True) or applied (False).
app은 An instance of Apps containing the state of the project after(before) the migration run. It should be used instead of the global apps registry to retrieve the models you want to perform operations on.
app에 대한 예시는 아래와 같다.
from django.apps import AppConfig
from django.db.models.signals import post_migrate
def my_callback(sender, **kwargs):
# Your specific logic here
pass
class MyAppConfig(AppConfig):
...
def ready(self):
post_migrate.connect(my_callback, sender=self)
django.core.signals.request_started
django.core.signals.request_finished
django.core.signals.got_request_exception
얘는 'core'에 있다. 위에서 model에서 직접 signal 함수를 호출했는데, 그럼 얘들은 어디서 호출해서, 우리가 정의한 임의의 callback 함수를 signal을 보내는 것일까?
사실 이 signal의 호출 부분을 우리가 제대로 catch하고 이해하는 것만으로도 Django 코어와 설계 자체에 대한 이해를 높일 수 있다. request_finished는 사실 쉽게 예측이 간다. HttpResponse 와 관련된 것을 추적하면 알 수 있을 것이다.
하지만 request_started 는? 과연 django는 이 start point을 어떻게 잡고 있을까? 바로 django.core.handlers.wsgi > WSGIHandler class
에서 __call__
부분에서 signals.request_started.send(sender=self.__class__, environ=environ)
를 확인할 수 있다. call 이 뭐였더라? 하시는 분 빠르게 클릭, WSGI에 대한 글은 해당 시리즈 글에서 확인이 가능하다.
즉 Django 어플리케이션 main entry point가 WSGIHandler Class
인 것을 알 수 있고, 이 class의 instance가 each request 마다 __call__
을 한다는 것 도 알 수 있다. 그리고 그 call 로 인해서 request_started
로 우리가 정의한 callback함수를 호출할 수 있다.
request_finished
는 django.http.response.HttpResponseBase > def close
에서 signals.request_finished.send(sender=self._handler_class)
로 호출이 된다.
Django가 HTTP 요청 처리를 시작할 때 전송된다. sender는 django.core.handlers.wsgi.WsgiHandler
와 같이, 요청을 처리한 핸들러 클래스가 파라미터로 넘어온다.
environ는 request 에 제공되어지는 dict 형태의 환경 변수 값이 저장되어 있다. dir로 찍어보면 알겠지만, REQUEST_METHOD, HTTP_HOST, PATH_INFO 등의 key가 있다.
HTTP(s) 요청을 처리하는 중 exception을 마주할 때마다 signal을 보낸다.
sender는 사용하지 않지만 보내는 값이고 항상 None을 가진다. 그리고 request를 전달하는데 HttpRequest obejct이다. 해당 HTTP(s) 요청에 사용된 request object이다.
신호에 대한 오해를 없애기 위해 비동기적으로 실행되지 않는다, 단순하게 callback 형태이다. 이를 실행할 백그라운드 스레드나 작업자가 없다!! 대부분의 Django와 마찬가지로 완전히 "동기식"이다.
Event-driven programming 컨셉만 비슷해서 그렇지, 절대 신호만 보내고 끝이 아닌, 비동기적으로 작동하지 않는다. (코어를 보면 확실하게 알 수 있다.)
정말 많은 도움이 되었습니다! :D