오늘 배운 것

어제 개발하던 알림 기능을 마저 개발해보려고 한다. 어제 job과 관련된 함수는 작성해 두었으니 이제 settings.py 파일에서 CRONJOBS 변수로 해당 함수를 등록하면 된다. 

 

crontab schedule에 관한 문서를 참고해서 아침 알림은 매일 오전 8시, 오후 알림은 오후 2시, 저녁 알림은 오후 8시에 가도록 설정해 주었다. 

# settings.py

CRONJOBS = [
    ('0 8 * * *', 'todos.jobs.send_morning_alarm'),
    ('0 14 * * *', 'todos.jobs.send_afternoon_alarm'),
    ('0 20 * * *', 'todos.jobs.send_evening_alarm'),
]

 

이제 두 번째 알림 케이스를 작업하면 된다. 투두 CRUD 중 C, U, D API가 호출되는 경우, 해당 API를 호출한 유저가 해당 디바이스 말고 또 다른 디바이스를 갖고 있을 경우, 해당 디바이스에 다시 Read API를 호출하라는 백그라운드 알림을 보내주는 작업이다. 

 

이 로직은 Todo, SubTodo, Category 모델의 Create, Update, Delete API의 끝에 들어가는 작업이기에, 해당 뷰 함수에 반복적으로 로직을 붙이는 것은 좋지 못하다고 판단했다. 그래서 생각해 본 방법으로는 미들웨어와 시그널이 있었다. 

 

내가 이해한 시그널은 가령 DB 특정 모델의 변화를 주목하고 있다가 변화가 일어났을 때 FCM으로 해당 유저에게 노티를 주는 방식이었다. 그래서 공식문서를 조금 읽어보던 중 signal은 implicit function call로 여러 쪼개진 로직에서 같은 이벤트를 주목할 때는 효과적일 수 있지만 그만큼 디버깅을 어렵게 할 수 있다는 문구를 보았다. 

 

그런데 지금 생각해보면 middleware의 사용 목적은 요청이나 응답의 값 등을 적절히 변형하는 용도인데, 이는 FCM 알림처럼 관련 모델 값이 변경되었을 때 노티를 받는 목적과는 맞지 않는다는 생각이 들었다. 

 

그래서 디버깅을 어렵게 할 수 있다는 가능성이 있지만 일단은 signal을 사용해보기로 했다. 그게 사용 목적에 더 맞고, 시그널이 DB를 보고 있으면 데이터가 변경될 때마다 알림이 가니 목적에는 가장 부합하게 구현할 수 있겠다는 생각이 들었다. 그리고 내가 사용하려는 모델이 변경되면 그에 맞게 노티를 주는 시그널은 알고보니 장고에 기본으로 구현되어 있어서, built-in signal을 사용하면 되었다. 

 

그런데 여기서 또 다른 해답을 얻었다. 공식문서에서는 정 필요한 경우에만 signal을 사용하고 아니면 다른 방법을 찾아보라고 하고 있었다. 새삼 signal이 코드를 디버깅하기 어려울 수 있다는 단점이 생각났다. 여기서는 model signal을 사용하는 대신 model manager를 사용해서 비슷한 작업을 처리하는 방법을 추천해주고 있었다. 

 

그래서 다시 signal 대신 manager를 사용하기로 했다. Todo, SubTodo, Category 모델의 manager에서 save 또는 create, update, delete 메소드만 변형하면 되겠다고 생각했다. models.Model의 save 메소드를 override해서 save 메소드 호출 후에 fcm 알림 로직을 추가해주는 방법을 생각했다. 그런데 이렇게 되면 fcm 알림 코드가 또 save 메소드 안에 종속되지 않나... 라는 생각이 들었다. fcm 알림은 명백히 save 메소드가 호출된 다음에 실행되어야 한다. 그러므로 signal의 목적에 부합한다. 

 

여러 고민을 하게 되었는데, 디버깅의 단점이 있지만 일단은 signal을 사용하기로 했다. 다행히 built-in signal에 post-save라는 기본 signal이 제공되어 있었다. 어떻게 사용하는지 잘 와닿지 않아서 'how to use post_save signals in django'로 검색했더니 나온 덕에 해답을 얻을 수 있었다. 

 

그런데 이렇게 했더니 또 다른 문제가 있었다. model이나 signal에서는 request.user이나 request.auth의 값을 갖고 있지 않다. 그렇기 때문에 이 요청이 어떤 디바이스로부터 온 것인지 알 수 있는 방법이 없다. 다음은 임시로 작성한 코드인데, signal은 파라미터로 request를 받지 않고 있다. 

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

@receiver(post_save, sender=Todo)
def update_todo_alarm(sender, instance, **kwargs):
    devices = instance.user.device__set.all()

 

즉 내가 원하는 것은 request.auth (또는 request.user)에 접근할 수 있으면서, signal과 비슷한 방법을 통해 이를 수행하는 것이었다. 찾아본 글에 의하면 signal은 순수 model 기능을 이용하기 때문에 불가능하다고 한다. 즉 request.auth에 접근하려면 순수 model 단에서는 불가능하고, 이는 manager도 마찬가지겠다. 

 

그렇다면 위에서 고려했던 다른 옵션인 미들웨어를 살펴봐야 하겠다. 다만 고려할 점은 요청은 그대로 끝나고 요청과 관계 없이, 즉 비동기로 미들웨어를 실행하는 게 맞아 보였다. 알림이 보내질 때까지 응답을 늦출 필요는 없기 때문이다. 

 

그러므로 비동기 미들웨어를 구현해보자. 공식문서를 보니 장고에서는 비동기 미들웨어도 지원하긴 했다. 다만 장고의 기본값은 동기 미들웨어만 지원하는 것이므로, boolean 변수나 decorator 등을 사용해서 이 기본값을 바꿔주는 과정이 필요했다. 공식문서의 예제를 가져와 보니 coroutine function (iscoroutinefunction) 이라는 말이 있는데 이 부분도 자세히는 몰라서 찾아보았다. 

from asgiref.sync import iscoroutinefunction
from django.utils.decorators import sync_and_async_middleware


@sync_and_async_middleware
def simple_middleware(get_response):
    # One-time configuration and initialization goes here.
    if iscoroutinefunction(get_response):

        async def middleware(request):
            # Do something here!
            response = await get_response(request)
            return response

    else:

        def middleware(request):
            # Do something here!
            response = get_response(request)
            return response

    return middleware

 

찾아보니 coroutine function이란 멀티태스킹처럼 실행을 미룰 수 있는 함수이며, exception이나 iterator를 구현할 때 유용하게 사용된다고 한다. 

 

그런데 이 비동기 미들웨어는 현재의 내가 이해하기에는 조금 복잡하고, 무엇보다 API를 보낼 때 FCM 알림을 보내는 것이 그렇게 큰 부담은 아니었다. 빨리 구현하는 것이 중요하니 우선은 기존처럼 동기 방식의 미들웨어로 작업하고, 시간이 될 때 다시 개선하는 것이 맞다고 판단했다. 

 

 궁금한 점

1. explicit function call과 implicit function call의 차이는 무엇일까

2. signal과 middleware가 같은 목적에서 사용할 수 있는 다른 방법이라는 생각이 들었다. 둘의 개념적 차이는 조금 알겠는데, 그러면 어떨 때 signal을 사용하고 어떨 때 middleware를 사용해야 할까? 내가 이해한 바로는 요청이나 응답에 관련된 작업은 middleware에서 하고, 흩어져 있는 코드들에 대해서 알림을 줄 때는 signal을 사용하라고 이해했다. 

3. coroutine function은 exception이나 iterator를 구현할 때 어떻게 사용될 수 있을까?

4. 비동기 미들웨어는 어떤 식으로 동작할까? 미들웨어가 연쇄적으로 얽혀 있으면 비동기 미들웨어를 사용하게 되면 미들웨어의 순서가 보장되지 않을 것 같다. 장고에서는 어떻게 이 문제를 해결할까?

 

+ Recent posts