[DRF]Signal을 사용하여 데이터 수정

Jay·2022년 10월 12일
0

배경

스포츠 게스트 매칭 서비스를 개발하다 알람기능을 구현하려고 하였다. 알람은 경기에 모집된 게스트 인원이 최소 인원 이상으로 모집이 완료되면 모집된 인원들에게 경기가 진행될 예정이라고 알려주는 기능이다. 우선 알람을 알려주기 전에 Alarm 모델의 레코드가 생성되는 방법을 어떻게 구현할까 알아보던 도중 아래와 같은 방법들을 생각하였다.

  1. 참여기록인 Participation 레코드가 생성될때, 해당 경기의 Participation 레코드의 개수를 계산하여 최소 인원 이상일 경우, Alarms 레코드를 생성한다.
  2. Signal을 사용하여 Participation 혹은 Game을 sender, Alarms를 receiver로 설정하여 최소 인원 이상일 경우, Alarm 레코드가 생성되도록 한다.

Signal

signal은 프레임워크 내의 분리된 애플리케이션끼리 이벤트가 발생하였을때 서로 알릴수 있도록 해준다. sender는 알려주는 쪽, receiver는 어떤 이벤트가 발생하였음을 sender로부터 알림받는 쪽이다.

django.db.models.signals

  1. pre_save / post_save : django 모델의 save() 메소드가 호출되기 전/후에 receiver에게 signal이 전달된다.
  2. pre_delete / post_delete: django 모델의 delete() 메소드가 호출되기 전/후에 receiver에게 signal이 전달된다.
  3. m2m_changed : 모델의 ManyToManyField 가 변경되었을때 receiver에게 signal이 전달된다.
  4. request_started / request_finished : HTTP request 요청 전/후에 receiver에게 signal이 전달된다.

Listening Signal

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

시그널을 받기 위해서는 signal.connect() 메소드를 사용하여 receiver 함수를 등록해야한다. receiver 함수는 시그널이 전송될때 호출되며, 시그널의 receiver 함수들은 등록된 순서대로 호출된다.

  • receiver : 시그널과 연결될 콜백 함수.
  • sender : 시그널을 받을 sender를 명시한다.
  • weak : signal 핸들러를 weak reference로 참조. 따라서 가비지 콜렉터에 의해 처리된다.
  • dispatch_uid : 같은 시그널이 여러번 전달되는 것을 막기 위해 receiver에게 고유 id를 설정한다.

Receiver functions
def my_callback(sender, **kwargs):
	# code

모든 receiver 함수/메소드는 sender와 **kwargs 를 인자로 받는다. 모든 시그널은 키-값으로 매핑된 인자들을 함께 보낸다. kwargs에는 생성/수정된 모델 인스턴스, 생성여부 등 이벤트에 관한 다양한 정보가 담겨져있다.


Connecting receiver functions

리시버를 시그널로 연결시키는 방법은 두가지가 있다. 직접 연결을 생성하는 방법과 리시버에서 명시적으로 리시버 함수에서 표기해주는 방식이다.

from django.core.signals import request_finished

request_finished.connect(my_callback)

위는 connection을 직접 생성하는 방식이다. request_finished 는 매 요청이 종료될때마다 리시버에게 시그널을 보내는 것을 의미한다. my_callback은 리시버 함수로, 위의 경우에는 매 요청이 종료될때마다 리시버 함수가 실행되도록 설정하였다.

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

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

위는 리시버에서 받을 시그널과 sender를 설정하여 신호를 받는 방식이다. @receiver 데코레이터를 사용하여 연결을 생성하였고, 시그널이 전송되는 조건을 데코레이터에 명시해주었다. receiver 함수에서는 sender를 명시하여 시그널을 받을 sender를 정의할 수 있다.

모든 receiver 함수/메소드는 sender와 **kwargs 를 인자로 받는다. 모든 시그널은 키-값으로 매핑된 인자들을 함께 보낸다. kwargs에는 생성/수정된 모델 인스턴스, 생성여부 등 이벤트에 관한 다양한 정보가 담겨져있다.



적용

구현할 시그널은 크게 2가지이다.

  1. 경기 참여 신청이 발생한 경우, 해당 경기의 현재 참석 인원을 1명 증가시킨다.
  2. 경기의 현재 참석 인원이 최소 모집 인원보다 커지게 되는 경우, 해당 경기의 참여자들에게 전송할 알람을 생성한다.
1. 경기 참여 신청 발생

경기 참여 신청 모델은 Participation 모델이다. 참여 신청 객체가 생성되는 경우, 해당 객체가 참조하는 경기 모델 객체의 현재 참여 인원수를 1씩 증가해준다.

signals.py

from django.dispatch import receiver
from django.db.models.signals import post_save, post_delete

from .models import Participation


@receiver(post_save, sender=Participation)
def _post_save_receiver(sender, **kwargs):
    print(kwargs)

우선은 Participation 모델 객체가 저장될 때, 신호가 올바르게 받는지 확인하기 위해 receiver를 생성하여 연결시켜주었다. 연결은 post_save로 설정하여 해당 모델에 save() 메소드가 호출될 때 시그널을 받고, sender는 Participation 모델로 설정하였다. receiver 메소느는 어떤 인자들을 받는지 확인하기 위해 kwargs를 출력해보았다.

{'signal': <django.db.models.signals.ModelSignal object at 0x000001785DE454F0>, 'instance': <Participation: Participation object (121)>, 'created': True, 'update_fields': None, 'raw': False, 'using': 'default'}

위와 같이 save()를 호출한 Participation 모델 객체와 생성여부, update로 인한 save() 호출시에는 업데이트된 필드도 함께 출력해준다. 이를 사용하여 receiver 함수를 구현해주었다.

@receiver(post_save, sender=Participation)
def _post_save_receiver(sender, instance, created, **kwargs):
    if created:
        game = instance.game
        game.player -= 1
        game.save(update_fields=['player'])

새로운 객체가 생성될 경우에만 해당 경기의 player 수를 늘려주도록 구현하였다.


2. 최소 인원 모집 완료 알람 생성

Game 모델 객체에는 경기가 실행되기 위한 최소 모집 인원 필드인 min_invitation과 현재 모집된 사람 수를 저장하는 필드인 player 속성이 존재한다. 이 두 필드를 사용하여 최소 인원이 모집 완료될 경우, Alarm 객체를 생성하도록 구현해보려고 한다.

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

from .models import Alarm
from games.models import Participation, Game

@receiver(post_save, sender=Game)
def create_alarm_objs(sender, instance, created, update_fields, **kwargs):
    if not created and 'player' in update_fields:
        game = instance
        if game.is_fulfilled():
            for participation in Participation.objects.filter(game=game):
                obj, is_created = Alarm.objects.get_or_create(game=game, user=participation.user)
                

이번에는 Game모델의 객체에서 save() 메소드가 호출될 때 시그널을 받는 리시버이다. instance로는 save()를 호출한 객체가 주어지고, 만약 player 필드가 수정될 경우 최소 인원 모집 완료 여부를 확인하는 is_fulfilled() 메소드를 호출하게 된다. 만약 모집이 완료된 경우에는 해당 경기 참여 신청인 Participation 객체들을 가져와서 해당 사용자들에게 보낼 Alarm 모델 객체를 생성하여 저장하게 된다.

class Game(models.Model):
	...
    
    def is_fulfilled(self):
        return self.min_invitation == self.player


다음 할 일

우선은 signal을 사용하여 필요한 기능을 구현해보았다. 하지만 위의 경우에는 Participation에서 Game으로 시그널을 보내고, Game에서 다시 Alarm으로 시그널을 보내게 된다. 또한 마지막으로 시그널을 받는 리시버에서 Participation 모델 queryset을 참조하여 for문으로 객체를 생성하기 때문에, 경기 참여 신청 api에서 상당한 오버헤드가 발생한다. 이러한 문제는 비동기를 사용하여 해결할 필요가 있어보인다.




reference

https://docs.djangoproject.com/en/4.1/topics/signals/

0개의 댓글