ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Django 비즈니스 로직을 분리하는 4가지 방법
    💻 프로그래밍/Django 2021. 2. 13. 06:07

    안녕하세요! 오늘은 Django에서 비즈니스 로직을 분리하는 방법들에 대해 말해보려고 합니다!

    최근에 개발을 하면서 비지니스로직에 대한 고찰을 해보게 되었고. 여러 블로그들을 참고하면서 그 블로그들을 통해 배운 내용들과 제 생각들을 함께 정리해보겠습니다.

     

    0. 들어가기전


    Django 는 완벽한(?) MVC 패턴의 프레임워크입니다. Model, View, Controller 로 나누어져 있으며 정확히는 Model, Template(View), View(Controller) 입니다. (살짝 용어가 다름)

    MVC 패턴 설명

    MVC 패턴 덕분에 Django 뿐만 아니라 여러 프레임워크에서 코드 재사용, 프로젝트 구조 파악 등을 쉽게 할 수 있습니다.(이하 MTV)

     

    간단히 Django의 MTV에 대해 설명해보면

    Model:  데이터베이스에 저장되는 테이블들을 Class 형태로 관리할 수 있다.

    Template: User(사용자)에게 보여주는 화면들에 대한 로직을 관리한다 (ex: html, javascript)

    View: Model과 Template에 필요한 로직들을 컨트롤하는 곳이다. Template에 랜더링 할 데이터들을 업데이트하거나 ORM(Model)을 사용해 DB에 CRUD를 실행한다.

    여기까지는 대부분 아는 내용이라고 생각합니다. 문제는 우리가 실제로 어떤 프로젝트를 진행할 때 생성되는 비즈니스 로직을 어디에 넣는 게 좋을까? 혹은 어디에 넣을 수 있을까?라는 고민에서 이 글을 쓰게 되었습니다.

     

    아마 저는 이 글을 쓰기 전까지는 그냥 View에 비즈니스 로직을 넣는게 일반적(?) 이라고만 생각했습니다. 그렇게 알고 개발을 해오기도 했고요 ㅎㅎ 하지만 알게 모르게 Django의 디자인 패턴(MTV) 안에서 다양하게 비지니스 로직의 위치를 정할 수 있는 방법들이 있었고 그것들을 정리해보면 이렇습니다.

    1. View, Form, Serializer
    2. Fat Models 
    3. Service layer
    4. Queryset/ Manager

    이제 각각의 방법들에 대해서 한번 알아보겠습니다.

     

    1. View, Form, Serializer 🏝


    개인적으로 가장 일반적으로 비즈니스 로직을 넣는 위치라고 생각합니다. 

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
     
    class HotelBookViewSet(mixins.CreateModelMixin,
                      mixins.ListModelMixin,
                      GenericViewSet):
        """
            - Hotel 예약 및 예약취소 관련 api
           endpoint : /hotels/:hotel_pk/books/
        """
     
        queryset = HotelBook.objects.all()
        serializer_class = HotelBookSerializer
     
     
        def create(self, request, *args, **kwargs) -> Response:
            data = request.data
            
            # 예약한 유저의 나라에 따라 가이드를 설정 해주는 로직
            if request.data.get('country'== 'kr':
                request['hotel_guide'= '홍길동'
                request['hotel_guide_phone'= '010-1234-5678'
            else:
                request['hotel_guide'= 'David Kim'
                request['hotel_guide_phone'= '010-7777-5678'
            
            serializer = self.get_serializer(data=data)
            serializer.is_valid(raise_exception=True)
            serializer.save()
     
            return Response(my_mode, status=status.HTTP_201_CREATED)
     
    cs

    위 예제처럼 특정 호텔에 사용자가 예약을 하는 API가 있습니다. 사용자의 국적에 따라 가이드를 연결해주는 비지니스 로직을 View에 넣을 수  있습니다. 여기서 좀만 더 리팩터링 해보면 Serializer.create() 를 오버라이딩 해서 가이드를 연결해주는 비즈니스 로직을 넣어줄 수 있습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    # views.py
     
    class HotelBookViewSet(mixins.CreateModelMixin,
                           mixins.ListModelMixin,
                           GenericViewSet):
        """
            - Hotel 예약 및 예약취소 관련 api
            endpoint : /hotels/:hotel_pk/books/
        """
     
        queryset = HotelBook.objects.all()
        serializer_class = HotelBookSerializer
     
     
        def create(self, request, *args, **kwargs) -> Response:
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            serializer.save()
     
            return Response(my_mode, status=status.HTTP_201_CREATED
     
     
     
    # serializers.py
     
    class HotelBookSerializer(serializers.ModelSerializer):
     
        class Meta:
            model = HotelBook
            fields = '__all__'
     
        def create(self, validated_data):
             # 예약한 유저의 나라에 따라 가이드를 설정 해주는 로직
            if validated_dataget('country'== 'kr':
                validated_data['hotel_guide'= '홍길동'
                validated_data['hotel_guide_phone'= '010-1234-5678'
            else:
                reqvalidated_dataest['hotel_guide'= 'David Kim'
                validated_data['hotel_guide_phone'= '010-7777-5678'
            
            ...
            
    cs

    이렇게 하면 좀 더 깔끔하게 보입니다. 하지만, View, Serializer, Form에 비지니스 로직을 위치하는 방법이 완벽하다고는 할 수는 없습니다. Serializer, Form은 data의 유효성을 검증(Validation)하는 class 들입니다. 

     

    Business logicValidation logic이 함께 있는 게 그렇게 좋다고(?)는 할 수 없을 것 같습니다. 그렇다고 View에 비즈니스 로직을 넣게 되면 로직이 커질수록 View가 지저분 해지고 View를 직관적으로 파악하기가 힘듭니다.

     

    그렇지만 일반적으로(저의 경우) 이렇게 많이 사용합니다.

     

    2. Fat Models 🐷


    Fat Models이라는 용어가 정확히 MTV패턴에서 나오는 말인지 Ruby on Rails(Ruby도 MVC 패턴) 에서 나온 용어인지 모르겠지만, 풀이하자면 말 그대로 Model에 비즈니스 로직을 넣어 크게 만든다는 의미입니다. 코드로 한번 살펴보겠습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    # views.py
     
    class HotelBookViewSet(mixins.CreateModelMixin,
                           mixins.ListModelMixin,
                           GenericViewSet):
        """
            - Hotel 예약 및 예약취소 관련 api
            endpoint : /hotels/:hotel_pk/books/
        """
     
        queryset = HotelBook.objects.all()
        serializer_class = HotelBookSerializer
     
     
        def create(self, request, *args, **kwargs) -> Response:
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            hotel_book = serializer.save()
            hotel_book.set_guide(request.data)
            
            return Response(my_mode, status=status.HTTP_201_CREATED
     
     
     
    # models.py
     
    class HotelBook(TimeModelMixin, Model):
        hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE) # 예약할 호텔 PK
        hotel_guide = models.CharField(max_length=200, blank=True, default=''# 호텔 가이드 이름
        hotel_guide_phone = models.CharField(max_length=200, blank=True, default=''# 호텔 가이드 전화번호
        reservation = models.ForeignKey(User, on_delete=models.CASCADE) # 예약한 사람
     
        class Meta:
            verbose_name = 'hotel_book'
     
        def set_guide(self, data):
            # 예약한 유저의 나라에 따라 가이드를 설정 해주는 로직
            if data.get('country'== 'kr':
                self.hotel_guide = '홍길동'
                self.hotel_guide_phone = '010-1234-5678'
            else:
                self.hotel_guide = 'David Kim'
                self.hotel_guide_phone = '010-7777-5678'
     
            self.save()
     
        ...
     
    cs

    위 코드를 보면 model에 비즈니스 로직이 들어가 있는걸 볼 수 있습니다. 예제가 조금 이상하긴(?) 하지만, 이런식으로 model에 함수로 비즈니스 로직을 처리 할 수 있도록 만들 수 있습니다.

     

    물론 model안에서 비지니스 로직을 처리할 수 있다는 장점이 존재하지만, 서비스가 커지게 되면 model에 많은 비지니스 로직이 들어가 복잡하게 되고, 다른 model에서 상속을 하는 경우가 있다면 모든 비지니스 로직을 상속하게 됩니다. 이런 경우 추상화(Abstarct)가 되어있지 않다면 사이드 이슈가 생길 수도 있습니다.

     

    결국 프로젝트, 비즈니스 로직이 커지고 복잡해지게 되면 model을 분리해야 하는 레거시 코드가 될 수도 있습니다. (한 app에서 많은 양의 코드가 있어서 복잡하기 때문에 유지보수 측면에서 안 좋을 수밖에 없습니다.)

     

    3. Service Layer 🚀


    개인적으로 Service Layer가 신기하기도 하고 디자인 패턴적인 면에서 괜찮다(?)라는 생각을 했습니다. 비즈니스 로직에 대한 내용을 Service Layer로 빼내는 것입니다. 예를 들자면 services.py 에 비지니스 로직을 넣는 것입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    # views.py
     
    class HotelBookViewSet(mixins.CreateModelMixin,
                           mixins.ListModelMixin,
                           GenericViewSet):
        """
            - Hotel 예약 및 예약취소 관련 api
            endpoint : /hotels/:hotel_pk/books/
        """
     
        queryset = HotelBook.objects.all()
        serializer_class = HotelBookSerializer
     
     
        def create(self, request, *args, **kwargs) -> Response:
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            hotel_book = serializer.save()
            
            # Serivice 쪽에서 비지니스 로직을 처리
            hotel_booking = HotelBooking()
            hotel_booking.set_guide(hotel_book)
            
            return Response(my_mode, status=status.HTTP_201_CREATED
     
     
     
    # services.py
     
    class HotelBookking(object):
        
        def set_guide(hotel_book):
            # 예약한 유저의 나라에 따라 가이드를 설정 해주는 로직
            if hotel_book.reservation.country == 'kr':
                hotel_book.hotel_guide = '홍길동'
                hotel_book.hotel_guide_phone = '010-1234-5678'
            else:
                hotel_book.hotel_guide = 'David Kim'
                hotel_book.hotel_guide_phone = '010-7777-5678'
     
            hotel_book.save(updated_fields=['hotel_guide''hotel_guide_phone'])
     
    cs

    이렇게 Service Layer에서 처리하게 되면 model, view(serializer, form) 모두 깔끔해집니다. 개인적으로 괜찮은 방법이라고도 생각됩니다.

     

    하지만, Service Layer에서 비즈니스 로직을 처리하는게 모두에게 일반적(?)이고 쉽지 않을 수 있습니다.

     

    일단 Django에서 이런 Service Layer를 추가하는 패턴자체가 정의 되어 있지않고 Service Layer에서 모든 비즈니스 로직을 처리하는것 보다 view, model 에서 비지니스 로직을 처리하는 게 django 기본 디자인 패턴에 부합되고, Service Layer보다 구현하기 쉬워 보입니다. 

     

    어떻게 보면 단순히 길어지는 로직을 다른 파일로 뺀 게 다인 것처럼 보일 수도 있고, 처음 Service Layer를 본 사람들에겐 다소 이해하기 힘들 수도 있을 것 같습니다.

     

    그래도 회사 코딩 컨벤션처럼 "이런 패턴으로 사용한다" 가이드가 있고, 대규모 프로젝트 혹은 서비스가 점점 복잡해질수록 이렇게 Service Layer로 나누는 게 좋은 이점이 될 수도 있을 것 같습니다.

     

    추가로 Test code를 작성하기도 쉽습니다. 비즈니스 로직이 View, Model에 있게 되면 Test Code를 구현하기 애매한 부분들이 있는데 Service Layer로 나누게 되면 Unit Test 자체도 매우 간단하게 구현할 수 있습니다. 

     

    4. Queryset/ Manager ⚙️


    약간 Model에 비즈니스 로직을 넣는 것과 비슷한데, Django Manager를 사용해서 ORM을 통해 비지니스 로직을 처리하는 방법입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
     
    class BookQuerySet(QuerySet):
        
       def set_guide(self):
           # 예약한 유저의 나라에 따라 가이드를 설정 해주는 로직
           if self.reservation.country == 'kr':
               self.hotel_guide = '홍길동'
               self.hotel_guide_phone = '010-1234-5678'
           else:
               self.hotel_guide = 'David Kim'
               self.hotel_guide_phone = '010-7777-5678'
     
           return self.save(updated_fields=['hotel_guide''hotel_guide_phone'])
     
     
    class HotelBook(TimeModelMixin, Model):
        hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE) # 예약할 호텔 PK
        hotel_guide = models.CharField(max_length=200, blank=True, default=''# 호텔 가이드 이름
        hotel_guide_phone = models.CharField(max_length=200, blank=True, default=''# 호텔 가이드 전화번호
        reservation = models.ForeignKey(User, on_delete=models.CASCADE) # 예약한 사람
     
        class Meta:
            verbose_name = 'hotel_book'
     
        objects = BookQuerySet.as_manager()
     
    cs

    이렇게 하면

    HotelBook.objects.filter(id=1).set_guide()

    뭐 이런 식으로 view나 다른 곳에서 사용 가능합니다. (살짝 예제가 이상하긴 하지만 맥락만... 이해해주세요 ㅎㅎ)

     

    이 방법의 장점은 queryset은 django에서 많이 사용되기 때문에 사용하기 편리합니다. 그리고 queryset으로 Test code를 작성하기도 편합니다. 그리고 한 번에 여러 데이터에 대한 데이터들을 바꿀 때 n번의 쿼리를 1번으로 처리할 수 있습니다.

     

    물론 여기에도 단점이 있습니다. n번의 쿼리를 1번으로 처리한다는 게 반대로 몇몇의 모델, 여기서는 HotelBook의 데이터를 변경하는데 다소 난해 해지는 면이 있습니다. 추가로 비즈니스 로직을 구현하면서 다른 model이나 3rd party API들을 호출하는 경우들이 생기면... 비지니스 로직을 구현하기 까다로워질 것입니다.

     

     5. 마치며


    개인적으로 Service Layer가 유지보수 측면이나 비지니스 로직을 구현하는 데 있어서 깔끔하고 좋은 방법이라고 생각합니다. 하지만,

    No Silver Bullet!

    이 중 어떤 방식을 사용하더라도 완벽하게 모든 문제를 해결할 수 있는 Silver Bullet은 없습니다. 프로젝트를 진행하면서 규모와 협업 등을 고려하며, 최적의 방식을 찾아나가는 게 좋은 것 같습니다.

     

    그리고 위 4가지 방법에 대해 이해하고 있다면 Django에서 비즈니스 로직을 구현하는데 좀 더 도움이 될거라고 생각합니다. 저의 경우는 위 내용은 개별적으로는 (3번빼고) 알고 있던 내용이지만, 이렇게 비지니스 로직을 구현하는 관점으로 보았던 적은 없던 것 같습니다.

     

    위 4가지 방법에 대해서 정리하면서, 앞으로는 개발할 때 비즈니스 로직을 적절한 위치에 넣는걸 한 번 더 고민하고 생각해볼 것 같습니다. 제가 정리한 내용에 대해 잘못된 부분이 있거나 더 나은 표현방법(?)등이 있다면 댓글 남겨주세요!!! 

     

    그럼 오늘도 즐코딩 하시길!

     

     

    *참고

    sunscrapers.com/blog/where-to-put-business-logic-django/

    댓글

운동하는 개발자 JAY-JI