오늘 배운 것

어제에 이어 알림 기능을 개발해 보려고 한다. 그러려면 서버에서는, CRUD를 통해 데이터를 변경시킨 클라이언트와 같은 계정을 공유하는, 모든 디바이스토큰을 가진 기기로 알림을 보내야 한다. 그러니까 이런 식이다. 

Device.objects.filter(user_id=user_id)

 

그런데 생각해보면 만약에 유저 계정 A1에 대해서 디바이스 B1과 B2가 있다고 하자. B1에서 C, U, D를 통해 투두 데이터에 변경이 일어나면 B1에 해당하는 유저 계정인 A1을 가진 디바이스 B1과 B2에게 알림을 보내게 될 것이다. 그런데 생각해보면 B1은 변경을 발생시킨 디바이스 주체이므로 굳이 B1에게 알림을 안 가게 하는 것이 맞는데, B2에게 알림을 보내면서 B1에게도 알림이 가게 된다. 이는 불필요한 알림일 수 있다. 

 

알고보니 request.auth라는 요청의 속성을 통해서 토큰의 커스텀 클레임을 볼 수 있다고 한다. 앞서 이런 상황을 대비하진 않았지만 혹시 몰라서 커스텀 토큰에 device 필드를 추가해 두었었다. 이 토큰값을 잘 받아서 디바이스를 식별할 수 있겠다.

 

그런데 request.auth가 토큰 클레임이 있는 딕셔너리 형태가 아니라 그냥 raw token으로 나왔다. 물론 별도로 토큰을 decode해서 디바이스 토큰을 꺼내볼 순 있겠지만 매 뷰에서 이걸 반복적으로 할 수는 없으므로, 미들웨어 단에서 처리하거나 simplejwt 라이브러리의 설정, DRF JwtAuthentication의 설정 등을 바꿔서 뷰에서는 request.auth로 클레임 값을 접근할 수 있도록 해야겠다. 이를 위해서 커스텀 인증 클래스 authentication.py를 만들고 기존 DRF의 JwtAuthentication을 상속받은 뒤, authenticate 메소드만 재정의했다. 

 

그리고 알고보니 authenticate 메소드에서 리턴하는 두 번째 값이 뷰에서 접근할 수 있는 request.auth의 값으로 지정된다고 해서, 이 값을 validated_token에서 token_payload로 바꿔주었다. 

from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken
from rest_framework_simplejwt.settings import api_settings
from jwt import DecodeError, ExpiredSignatureError
import jwt

class CustomJWTAuthentication(JWTAuthentication):
    def authenticate(self, request):
        raw_token = self.get_raw_token(self.get_header(request))

        if raw_token is None:
            return None

        try:
            validated_token = self.get_validated_token(raw_token)
            token_payload = jwt.decode(
                raw_token, 
                api_settings.SIGNING_KEY, 
                algorithms=[api_settings.ALGORITHM]
            )
            request.auth = token_payload
        except (InvalidToken, DecodeError, ExpiredSignatureError) as e:
            raise InvalidToken(e)

        return self.get_user(validated_token), token_payload

 

그랬더니 request.auth에서 딕셔너리 값으로 토큰 클레임을 잘 볼 수 있었고, 커스텀 필드로 정의한 'device' 필드도 볼 수 있었다. 

 

그러면 이제 해당 디바이스 토큰 값을 가지고 알림 로직을 호출할 수 있다. 그런데 생각해보니 알림 로직을 호출하는 필드도 뷰마다 중복이 많이 될 것 같았다. 이럴거면 응답 관련해서 미들웨어를 써도 되지 않나? 아니면 뷰가 다 동작한 다음에 해당 디바이스 토큰과 뷰의 메소드로(GET일 경우 해당되지 않으므로) FCM 알림을 보내는 로직을 만드는 다른 방법이 있을까?

 

우선 찾아보니 미들웨어를 쓰는 방법이 있었고, 예상하지 못했던 또 다른 방법은 장고의 signal(시그널)을 쓰는 것이었다. 미들웨어는 이제 API로 들어온 요청에 대한 응답을 할 때 사용하는 방법이고, 시그널은 장고 모델에 대해서 변경 사항이 생길 때 변경사항이 생긴 시점 이전이나 이후에 특정 로직을 수행하도록 해 주는 장고의 기능으로 알고 있다. 

 

공식문서를 참고하니 시그널은 어떤 이벤트가 발생했을 때 그 이벤트의 발생 전이나 후에 실행되는 함수를 연결해주는 기능이다. 클라이언트에서 사용하는 콜백 함수와 유사한 개념으로 보였다. 

 

그렇다면 이런 상황에서 시그널을 사용하는 게 맞을까, 미들웨어를 사용하는 게 맞을까? 시그널을 사용해서 DB 모델에 변경이 생기면 FCM 알림을 보내주는 라이브러리는 없을까 싶었다. 

 

아니나 다를까, 바로 있다. fcm_django라는 라이브러리가 있단다. 일단 설치를 해 보자. 

pip install fcm_django

 

settings.py의 INSTALLED_APPS에도 다음과 같이 추가해 주었다.

# settings.py

INSTALLED_APPS = [
	# ...
    'fcm_django',
]

 

알고보니 해당 라이브러리에서는 FCMDevice라는 모델을 만들어서 DB에 각 유저들의 디바이스와 FCM 관련 정보를 저장하는 용도로 사용하는 것 같았다. 또한 해당 FCMDevice의 장고 매니저 기능을 사용하면 send_message 메소드를 통해 해당 디바이스로 메시지를 보낼 수 있다..! 딱 내가 원하던 거였다. 

 

그런데 생각해보니 기존에 정의한 Device라는 모델에서 이미 유사한 정보를 다루고 있었다. 이럴 경우에는 Device 모델을 제거해야 하겠다. 

 

그런데 로컬에서 무턱대고 RDS에 Device 테이블을 지우는 마이그레이션을 실행해 버리면 에러가 날 게 분명했다. 이미 ECS에서 돌아가고 있는 서버에 있는 코드는 RDS에 Device 테이블이 있다고 생각하고 이를 사용하고 있기 때문이다. 그렇다면 어떻게 해야 할까. 

 

우선은 코드에서 Device 테이블에 대한 dependency를 아예 제거해야 하겠다. 그리고 해당 코드가 develop 브랜치에 머지되고 완전히 ECS 서버에 올라가면 그때 지워야 하겠다. 그런데 또 생각해보니 이미 Dockerfile에서 'python manage.py makemigrations && python manage.py migrate' 명령어를 실행하고 있다. 그러므로 해당 코드가 정상적으로 실행된다면 이미 RemoveModel 마이그레이션이 실행되어 RDS에서 테이블이 잘 삭제되었을 것이다. 결론은 걱정할 필요가 없었다.

 

그리고 찾아보니 fcm_django 라이브러리에 ViewSet이 있었다. FCMDeviceViewSet은 FCMDevice를 등록하는 데 사용하는 ViewSet이었고, FCMDeviceAuthorizedViewSet은 FCMDevice의 정보를 변경하는 데 사용되는 ViewSet이라고 이해했다. 그렇다면 이 ViewSet들을 사용해서 충분히 커버가 가능하겠다. 그런데 해당 ViewSet을 프론트에서 디바이스 토큰을 등록이나 변경하기 위해 별도로 호출해야 할지, 아니면 구글로그인 API를 호출하면 구글로그인 뷰에서 해당 ViewSet을 호출해야 할지는 잘 모르겠었다. 

 

그런데 항상 흐름은 구글로그인이 성공적으로 완료된 다음에 디바이스 토큰을 조회하는 것이므로, 이 흐름이 변화가 없다면 굳이 프론트에서 호출할 필요가 없다고 판단했다. 그래서 두 번째 방법을 사용하기로 했다. 

from fcm_django.api.rest_framework import FCMDeviceCreateOnlyViewSet

response = FCMDeviceCreateOnlyViewSet.as_view(request)

 

궁금한 점

1. 어떤 라이브러리를 사용할 때 INSTALLED_APPS에 해당 라이브러리 이름을 넣어야 하는 것과 안 넣어줘도 되는 기준이 궁금하다. 이걸 결정하는 부분의 코드가 있다면 한번 찾아보고 싶다. 

2. Mixins와 Viewset을 같이 상속받아서 어떻게 상속받은 별도의 두 클래스가 같이 동작하는지도 궁금하다. 

3. FCMDevice에서 objects 매니저를 이용해서 send_message 메소드를 호출하면 FCM 서버를 통해 메시지가 보내진다고 한다. 이 원리도 궁금하다. 

 

+ Recent posts