Django Signal - 시그널, db.models.signal, Publish/Subscribe 매커니즘

정현우·2022년 7월 21일
6

Django Basic to Advanced

목록 보기
26/36
post-thumbnail

Django Signal

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.

1. 시그널 기본 매커니즘

  • 분리된 app의 작업 발생을 알려고, 처리할 수 있도록 만들어진 기능

  • sender, signal, reciver의 형태로 되어 있으며, 특정 이벤트를 singnal이 포함하고 있으며, reciver가 sender로 부터 그 signal을 받는 것이다.

  • 관찰자 모드, 게시-구독(Publish/Subscribe)이라고도 부른다.

1) 먼저 실제 사용 예시

  • 매커니즘만 보고 장고 내장 시그널을 바로 보면 와닿지 않을 수 가 있다. 유저가 생성 될 때, 그 시그널을 받아서 바로 profile을 만드는 로직 을 시그널 통해서 만들어 보자.
# 우선 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을 조금 더 자세하게 살펴보자


2. Django 내장 signal

1) django에서 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) 를 조금 더 자세히 살펴보자.

    • receiver: 이 신호에 연결될 콜백 함수이다.
    • sender: 신호를 수신할 특정 발신자를 지정합니다.
    • weak: Django는 기본적으로 신호 처리기를 "약한 참조"로 저장한다. 따라서 recevier가 로컬 함수인 경우 가비지 컬렉션의 수집 대상이 될 수 있다. 이를 방지하려면 신호의 connect() 메서드 를 호출할 때 weak=False로 줘야한다. 약한 참조 더 알아보기
    • dispatch_uid: 중복 신호가 전송될 수 있는 경우 신호 수신기에 대한 고유 식별자다.

2) Model signals

  • 기본적으로 장고가 내장하고 있는 모델 관련 시그널은 아래와 같다.
  • signal 종류 중 이름이 pre는 해당 신호의 "전" 이고 post는 "후" 이다.
  1. django.db.models.signals.pre_init
  2. django.db.models.signals.post_init
  3. django.db.models.signals.pre_save
  4. django.db.models.signals.post_save
  5. django.db.models.signals.pre_delete
  6. django.db.models.signals.post_delete
  7. django.db.models.signals.m2m_changed
  8. django.db.models.signals.class_prepared
  • 기본적으로 위 예시에서 봤듯이 함수를 만들고 -> 파라미터값을 정해진 대로 세팅한 뒤 -> 시그널 종류의 함수에 connect 함수를 활용하여, 우리가 정의한 함수와 모델을 이어준다. 이게 기본으로 모든 종류의 시그널을 세팅하는 방법이다.

(1) pre/post init

  • 특정 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.

(2) pre/post save

  • 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의 의미다.

(3) pre/post delete

  • sender, instance, using attribute만 있으며, 설명은 위와 동일하다. 참고로 post_delete는 이미 해당 instance는 DB에서 지워진 상태 라는 점 기억해야 한다.

(4) m2m_changed

  • 엄밀히 말하자면 model class의 신호가 아니라 field의 신호기 때문에 조금 다르다.

  • m2m는 이름답게 N:M 관계를 위해 서로 다양하게 바뀐 경우를 생각한다. ManyToManyField 가 바뀌었을 때 보내는 signal이며 action 이라는 파라미터 값으로 "pre/post_add, pre/post_remove, pre/post_clear" 값을 받는게 핵심이다.

  • m2m은 특성상 직접 예제를 통해 모두 체크하는 것이 가장 정확하게 와닿을 것이다.

(5) class_prepared

  • 이 signal은 사실 응용프로그램에서 활용되는 것 보다 django가 내부적으로 사용하는 signal이다. 모델이 Django’s model system 에 최초로 정의되고 등록되었을 때 한 번 보낸다.

3) Management signals

  • 기본적으로 장고가 내장하고 있는 마이그레이트 관련 시그널은 아래와 같다.
  1. django.core.signals.pre_migrate
  2. django.core.signals.post_migrate
  • 이 migrate signal을 보내는 주체는 "django-admin" (python manage.py cli) 이다.

(1) pre/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)

4) Request/response signals

  • 기본적으로 장고가 내장하고 있는 request/reponse 관련 시그널은 아래와 같다.
  1. django.core.signals.request_started
  2. django.core.signals.request_finished
  3. 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_finisheddjango.http.response.HttpResponseBase > def close 에서 signals.request_finished.send(sender=self._handler_class) 로 호출이 된다.

(1) request_started

  • Django가 HTTP 요청 처리를 시작할 때 전송된다. sender는 django.core.handlers.wsgi.WsgiHandler 와 같이, 요청을 처리한 핸들러 클래스가 파라미터로 넘어온다.

  • environ는 request 에 제공되어지는 dict 형태의 환경 변수 값이 저장되어 있다. dir로 찍어보면 알겠지만, REQUEST_METHOD, HTTP_HOST, PATH_INFO 등의 key가 있다.

(2) request_finished

  • sender는 request_started 와 동일하다. 그 외 별다른 파라미터 값은 없다.

(3) got_request_exception

  • HTTP(s) 요청을 처리하는 중 exception을 마주할 때마다 signal을 보낸다.

  • sender는 사용하지 않지만 보내는 값이고 항상 None을 가진다. 그리고 request를 전달하는데 HttpRequest obejct이다. 해당 HTTP(s) 요청에 사용된 request object이다.

그 외

  • django.test.signals.setting_changed, django.test.signals.template_rendered, django.db.backends.signals.connection_created 가 있다.

3. Signal 사용 주의 사항

1) 쓰레딩이나, 비동기 작업이 아니다.

  • 신호에 대한 오해를 없애기 위해 비동기적으로 실행되지 않는다, 단순하게 callback 형태이다. 이를 실행할 백그라운드 스레드나 작업자가 없다!! 대부분의 Django와 마찬가지로 완전히 "동기식"이다.

  • Event-driven programming 컨셉만 비슷해서 그렇지, 절대 신호만 보내고 끝이 아닌, 비동기적으로 작동하지 않는다. (코어를 보면 확실하게 알 수 있다.)

2) 코드의 유지 관리가 어려울 수 있다.

(1) 쓸모없는 파일 분산이 된다.

  • 작성을 한 사람은 알 것이지만, 협업을 하는 사람 입장에서 call-back함수를 어디서 호출하는지 찾기 복잡할 때 가 많다. 정확한 docs가 되어 있지 않으면, 파일이 분산됨에 따라 signal 함수를 찾기 힘들다.
  • 특히 특정 모델에 대해 models 정의에 signal 함수가 정의되어 있는 것도 아니고, 해당 model이 시그널이 등록되어 있는지 아닌지도 찾기 힘들 수 도 있다.

(2) transaction의 원자성 헤칠 우려 -> 에러 핸들링의 어려움

  • 이 부분은 코딩을 어떻게 하느냐에 따라 달라지겠지만, 기본적으로 save된 뒤에 call-back으로 처리하기 때문에 해당 함수에서 sender와 디펜던시가 있는 특정 모델을 생성할 경우, 그리고 그 원자성을 보장해야 할 경우, rollback도 어렵다. 즉, 에러가 터졌을 때 sender instance 도 rollback을 해야하는 경우라면, 더욱 복잡해진다.

(3) 가독성 저하, (앞서 말한 부분과 어느정도 겹침)

  • 협업의 관점에서 굳이 굳이 signal을 사용할 필요가 없는 부분 마저도 signal을 사용하면, 역시 어떤 모델이 시그널에 등록되어 있는지 한 눈에 확인하기 어려워서, 특정 모델이 생성되고 삭제될 때 어떤 작업이 일어나는지 하나하나 디버깅 해야하고, 그 함수가 분리되어 있으면 또 follow-up을 해야한다.
  • 이런 부분에서 코드 전체 흐름이 파편화 되어서 가독성이 오히려 저하가 될 수 있다.

출처

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

4개의 댓글

comment-user-thumbnail
2022년 7월 31일

정말 많은 도움이 되었습니다! :D

1개의 답글
comment-user-thumbnail
2023년 7월 31일

최근에 알게 된 개념인데 상세하게 정리해주셔서 많은 도움이 되었습니다!

1개의 답글