Django에 Service layer 적용(MVSC) + Service Layer에 테스트코드 추가

런던행·2021년 3월 16일
2

Django 업그레이드

목록 보기
8/17

이전 글에서 장고에서 비지니스 로직을 다루는 4가지 방법에 대해서 소개를 하였다. 이번 글에서는 비지니스 로직을 어떻게 서비스레어로 분리할 것인가에 대해서 다루고자 한다.
필자는 주로 Laravel에서 비지니스 로직을 Service 레이어로 분리해서 작업을 했고, 자바 스프링에서도 가볍게 사용을 하였다.
Service 레이어로 분리하는 이유에 대해서 개인적으로 크게 2가지를 나열하자면

  • 관심사 분리에 용이
  • 테스트코드 작성 용이

상황에 맞게 서비스 레이어를 도입할 수 있고 아닐 수도 있다. 각 상황에 맞게 적용했으며 한다.

아래는 서비스 레이어를 도입하여 리팩토링 하는 과정이다.

예제 서비스 : 소개팅앱

아래 코드는 남성 사용자가 여성 사용자의 프로필을 요청했을 때 처리하는 뷰 코드다.

크게 3가지 기능을 한다.

  • 여성 사용자의 프로필 정보를 가져온다.
  • 여성 사용자 프로필에 방문했다는 기록을 저장한다.
  • 여성 사용자에게 누군가 프로필을 방문했다고 푸시를 보낸다.
class AccountViewSet(ModelViewSet):
    permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope]

    serializer_class = UserSerializer
    queryset = User.objects.all()
    pagination_class = StandardResultsSetPagination

	# 선택한 사용자 프로필 정보 가져오기
    def retrieve(self, request, *args, **kwargs):
        target_user_id = self.kwargs['pk']
        logged_user: User = self.request.user

        try:
            queryset = self.get_queryset().prefetch_related(
                'style_habit_info', 'basic_info',
                Prefetch('main_profile', queryset=FlexFile.objects.filter(name='profile').all()),
            ).get(id__exact=target_user_id)
            serializer = self.get_serializer(queryset)

            # 방문 기록을 저장
            history_visit: HistoryVisitProfile = HistoryVisitProfile()
            history_visit.target_user_id = target_user_id
            history_visit.visitor = logged_user
            history_visit.save()

            # 상대방에서 방문했다는 푸시를 알림 
            notify(target_user_id)

        except Exception as e:
            return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': str(e)})

        new_data = {}
        new_data.update(serializer.data)

        return Response(status=status.HTTP_200_OK, data=new_data)

코드가 간단하고 심플하기에 분석하기에는 아주 쉽다. 그러나 비지니스 요구사항이 많고 코드가 길어진다면 파악이 쉽지 않을 것이다.

첫 번째 리팩토링 (관심사 분리)

관심사를 크게 2개로 분리하고자 한다.

  • 방문 기록을 저장하는 일
  • 푸시를 보내는 일

각 관심사마다 서비스를 작성한다. 지금은 조금 초라할 수도 있지만 기능이 추가되고 여러번 리팩토링을 하면 더 깔끔해진다.
파일명은 services.py 로 정한다.


class HistoryVisitService:
    
    def check_in(self, from_user: User, to_user: User):
        history_visit: HistoryVisitProfile = HistoryVisitProfile()
        history_visit.target_user = to_user
        history_visit.visitor = from_user
        history_visit.save()
        
        
class NotificationService:
    
    def send_push(self, user: User):
        pass

두 번째 리팩토링 (레거시 수정)

class AccountViewSet(ModelViewSet):
    permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope]

    serializer_class = UserSerializer
    queryset = User.objects.all()
    pagination_class = StandardResultsSetPagination

	# 선택한 사용자 프로필 정보 가져오기
    def retrieve(self, request, *args, **kwargs):
        target_user_id = self.kwargs['pk']
        target_user = User.objects.get(id=target_user_id)
        logged_user: User = self.request.user

        try:
            queryset = self.get_queryset().prefetch_related(
                'style_habit_info', 'basic_info',
                Prefetch('main_profile', queryset=FlexFile.objects.filter(name='profile').all()),
            ).get(id__exact=target_user_id)
            serializer = self.get_serializer(queryset)

            # 방문 기록을 저장
            history_service: HistoryVisitService = HistoryVisitService()
            history_service.check_in(logged_user, target_user)

            # 상대방에서 방문했다는 푸시를 알림
            notification_service: NotificationService = NotificationService()
            notification_service.send_push(target_user_id)

        except Exception as e:
            return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': str(e)})

        new_data = {}
        new_data.update(serializer.data)

        return Response(status=status.HTTP_200_OK, data=new_data)

세 번째 리팩토링 (테스트코드 작성)

관심사를 2개 분리했고, 2개의 서비스를 작성하였다.
우린 2개의 서비스에 대해서만 테스트 코드를 작성한다.

tests/test_history_visit_service.py

import pytest

from account.models import User
from api.datting.services import HistoryVisitService
from datting.models import HistoryVisitProfile


@pytest.mark.django_db
def test_check_in():
    """
    프로필 열람기록을 저장 할수 있다.
    """

    # Given
    mock_user1 = User.objects.create_user('t@t.com', 'mock1', None)
    mock_user2 = User.objects.create_user('a@a.com', 'mock2', None)
    service: HistoryVisitService = HistoryVisitService()
    
    # When
    service.check_in(mock_user1, mock_user2)

    # Then
    history: HistoryVisitProfile = HistoryVisitProfile.objects.first()
    assert mock_user1.id == history.target_user.id
    assert mock_user2.id == history.visitor.id

관심사만 분리해서 테스트코드를 작성하면 비지니스을 중점으로 테스트할 수 있어 테스트코드 작성에 용이하다.
DTO까지는 개인적으로 안해도 될거같다.

레퍼런스
https://breadcrumbscollector.tech/how-to-implement-a-service-layer-in-django-rest-framework/

profile
unit test, tdd, bdd, laravel, django, android native, vuejs, react, embedded linux, typescript

0개의 댓글