ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Django] Proxy Model 사용하기
    내가쓴글/개발 2023. 7. 11. 19:00
    파이썬 장고

    머리말

    최근 Django로 구현된 프로젝트에서 사용하는 전반적인 DB 설계를 리팩토링 하면서, 구현 자체도 새롭게 해야할 부분이 많아서 전체 프로젝트를 재구성하고 있다.
    그 와중에 가장 최근 production에서 사용 중인 비즈니스 로직의 업데이트를 하면서 "이미 적용되어 있었다면 정말 편하고 좋았을텐데.." 라는 생각이 들었던 Proxy Model에 대한 설명을 적어보고자 한다.

    Proxy Model 외에도 그런 생각이 든 것들이 정말 많다ㅠㅠ 나중에 리팩토링이 끝난다면, 전반적인 내용에 대해 정리하면서 더 작성해보고 싶다.

    TLDR;

    type 값으로 특징(비즈니스 로직)이 구분되는 Model의 경우 Meta.proxy = True 옵션을 사용하면, 모델에 대한 설계를 더 명확히 할 수 있고, 나중에 해당 모델의 type과 관련된 업데이트가 필요해지더라도 훨씬 유연하게 대처할 수 있다. 물론 코드의 가독성도 올라간다. 구체적으로 어떤 상황에 어떤 코드로 사용하는지 궁금하신 분들은 계속 읽어보시길 바란다.

    추가적으로, 공식 문서의 내용이 궁금하신 분들은 여기에서 내용을 확인해보실 수 있다.


    본문

    Proxy Model

    공식 문서에서는 아래와 같이 표현하고 있다.

    When using multi-table inheritance, a new database table is created for each subclass of a model. This is usually the desired behavior, since the subclass needs a place to store any additional data fields that are not present on the base class. Sometimes, however, you only want to change the Python behavior of a model – perhaps to change the default manager, or add a new method.
    This is what proxy model inheritance is for: creating a proxy for the original model. You can create, delete and update instances of the proxy model and all the data will be saved as if you were using the original (non-proxied) model. The difference is that you can change things like the default model ordering or the default manager in the proxy, without having to alter the original.

    중요 내용만 정리하자면,

    • Model 상속은 원래 새로운 DB Table을 생성한다.
    • Table 추가 생성 없이 같은 Model에 대해 동작만 다르게 하는 무언가가 필요할 때 쓴다.
    • 그 중에는 순서 변경, 기본 매니저 변경 등이 있다.

    위 내용만 보면, Meta.managed = False로 설정한 unmanaged Model로도 같은 기능을 하는 Model을 만들 수 있을 것만 같은데, 왜 굳이 Proxy Model이 따로 존재해야 하는지 의문이 생길 수 있다. 공식 문서에서는 둘의 차이를 아래와 같이 기술하고 있다.

    With careful setting of Meta.db_table you could create an unmanaged model that shadows an existing model and adds Python methods to it. However, that would be very repetitive and fragile as you need to keep both copies synchronized if you make any changes.
    On the other hand, proxy models are intended to behave exactly like the model they are proxying for. They are always in sync with the parent model since they directly inherit its fields and managers.
    The general rules are:
    If you are mirroring an existing model or database table and don’t want all the original database table columns, use Meta.managed=False. That option is normally useful for modeling database views and tables not under the control of Django.
    If you are wanting to change the Python-only behavior of a model, but keep all the same fields as in the original, use Meta.proxy=True. This sets things up so that the proxy model is an exact copy of the storage structure of the original model when data is saved.

    정리하자면,

    • field 정보를 다르게 하고 싶다면, 즉, 모든 column을 필요로 하지 않는 경우, unmanaged Model을 쓰는게 맞다.
    • 그게 아니라면, Proxy Model을 사용하는 것이 원래 Table과 동일한 구조를 갖는 설계의도에 부합한다.

    즉 column 을 추가하면서 새로운 기능(비즈니스 로직)이 추가되는 경우는 이 글에서 소개하고자 하는 내용과는 거리가 있다. 하나의 type을 명시하는 column의 instance별 값에 의해 비즈니스 로직이 달라지는 Model에 대한 설명이 이 글에서 설명하고자 하는 경우라고 할 수 있다.

    추가적으로, 이 모든 설정들을 할 때 쓰이는 Django Model의 Meta 옵션들에 대한 공식 문서내용을 찾아보고 싶다면 아래를 확인해보길 바란다.

    문제 상황

    다음과 같은 Model이 있다고 가정해보자.

    class UserType(modelchoices.Choices):
        STUDENT = (1, "STUDENT")
        PARENT = (2, "PARENT")
        TEACHER = (3, "TEACHER")
        ADMIN = (4, "ADMIN")
    
    class CustomUser(models.Model):
        user_type = models.PositiveIntegerField(default=UserType.STUDENT)
        username = models.CharField(max_length=15,unique=True)
        full_name = modelsCharField(max_length=15)

    여기에서 User의 user_type은 권한을 포함한 다양한 비즈니스 로직에서 사용되는 조건이 될 것이다. 예를 들어,

    • STUDENT타입의 User는 자신의 full_name변경 권한이 없지만, TEACHER, PARENT, ADMIN은 가능하다던지,
    • 여기 Model에는 표현되어 있지 않지만, 매일 매일 PARENT들에게만 자신의 STUDENT에 대한 출석 정보를 메세지로 알려준다던지

    등의 다양한 경우들이 있을 것이다. 그 중에서도 특히 후자의 경우를 작동시키는 함수를 구현한다고 하면,

    def notify_daily_attendance():
        parent_queryset = CustomUser.objects.filter(
            user_type=UserType.PARENT).prefetch_related("parentinfo__child")
        return bulk_send_message(
            template="daily_attendance_notification",
            user_queryset=parent_queryset
        )

    위처럼 구현할 수 있을 것이다.

    PARENT 타입의 유저를 쿼리해야하는 경우가 여기 뿐이라면, 한 번 수고하면 그만이지만, 그게 아니라면, 쿼리가 필요한 매 순간마다, 우리는 filter에 옵션으로 user_type=UserType.PARENT라는 값을 넘겨줘야 한다. 백 번 양보해서, 구현할 때만 신경쓰면 되지 않느냐고 생각할 수 있다. 하지만, 비즈니스 로직은 새로운 기획과 함께 영향을 받을 수 있고, 유지 보수의 관점에서 본다면 매우 불편할 것임이 분명하다.

    • 첫째로, 모든 같은 쿼리를 하는 코드를 뒤져서 바꿔줘야 한다.
    • 둘째로, 테스트 커버리지가 100%라면 좋겠지만, 그렇지 못하다면, "혹시 어디서 빼먹지 않았을까" 라는 불안함을 떨쳐내기 쉽지 않다.

    해결 방법

    이 때 만약 ParentUser를 Proxy Model로 정의해 두었다면, 이야기는 달라진다.

    class ParentManager(models.Manager):
        def get_queryset(self):
            return super().get_queryset().filter(user_type=UserType.PARENT)
    
        def create(self, **kwargs):
            kwargs.update({'user_type': UserType.PARENT})
            return super().create(**kwargs)
    
    class ParentUser(CustomUser):
        objects = ParentManger()
        class Meta:
            proxy = True

    위처럼 해두면, 기존 쿼리가 필요없어지고, ParentUser.objects.all()filter없이 간단하게 원하는 결과를 얻을 수 있다. 또, ParentUser가 작동하는 모든 비즈니스 로직에 대해 변경(ADMIN타입의 User 에게도 메세지를 보내야 한다던지 등)이 일어나면, ParentUser 혹은 ParentManager 만을 수정해서 여기저기 코드를 수정할 필요없이 간단하게 적용해줄 수 있다.

    위에서 언급하지는 않았지만, create 메소드 또한 새로 작성해주었기 때문에, django admin을 활용하는 경우에도, CustomUser와는 별개의 인터페이스로 ParentUser를 관리할 수 있고, 물론 CustomUser생성 시에도 잘 활용하면 가독성을 높일 수 있다.

    하지만, Proxy Model을 사용할 때도, 조심해야할 부분은 있다. Proxy Model을 통한 역참조는 가능하지 않다는 것이다.

    class ArticleStatus(modelchoices.Choices):
        CREATED = (1, "CR")
        MODIFIED = (2, "MO")
        DELETED = (3, "DE")
    
    class Article(models.Model):
        author = models.OneToOneField(CustomUser, on_delete=CASCADE)
        status = models.CharField(max_length=2, default=ArticleStatus.CREATED)
        content = models.TextField(blank=True, default="")
    
    class ModifiedManager(models.Manager):
        def get_queryset(self):
            return super().get_queryset().filter(status=ArticleStatus.MODIFIED)
    
    class ModifiedArticle(Article):
        objects = ModifiedManager()
        class Meta:
            proxy = True

    조금은 억지스러운 예시이지만, 위의 ModifiedArticle에 대한 특정 비즈니스 로직이 존재한다고 했을 때, Proxy Model을 사용하지 않았다면,

    user = CustomUser.objects.first()
    articles = user.article_set.filter(status=ArticleStatus.MODIFIED)

    처럼 특정 User가 작성한 전체 ArticleMODIFIED상태의 것들을 쿼리했겠지만, Proxy Model을 사용했다고 해도,

    user = CustomUser.objects.first()
    articles = articles = user.modifiedarticle_set.all()

    처럼은 쿼리가 불가능하다. 대신,

    user = CustomUser.objects.first()
    articles = ModifiedArticle.objects.filter(user=user)

    처럼 쿼리해야 한다. 만약 articles = user.modifiedarticle_set.all()를 가능하게 하려면, CustomUser의 @propertymodifiedarticle_set을 설정해주는 방법이 있지만, 굳이? 싶다.

    유지 보수 및 코드 가독성을 올리기 위해서 사용하는 Proxy Model의 장점이 흐려지는 방식인 것 같아서 필자는 사용하지 않기로 했다.
    그 외에도 Proxy Model에 대해 찾아보면서 자체를 ForiegnKey로 사용하는 것에 대한 이슈도 있었는데, 필자는 사용하지 않는 방법이어서 나중에 필요해지면 조금 더 최신 정보를 찾아볼 생각이다.

    맺음말

    Proxy는 현재 구현되어 있는 우리 회사 비즈니스 로직을 간결하게 정리해줄 수 있는 좋은 수단으로 느껴졌고, 새로운 프로젝트에 적극 사용중이다. 나중에 시간이 된다면, Meta 옵션들을 활용한 전반적인 Model 구성 및 활용에 대한 시리즈를 작성해보고 싶다.
     

Designed by Tistory.