How to use Django rest framework

  • |
  • 26 December 2016
Image not Found

1. Django를 사용하게 된 배경과 적용 과정

버즈빌이 해당 프레임워크(Django)를 사용하게 된 지는 아마 한 1년 반 정도 전인 2015년 봄쯤이었던 것으로 기억합니다.  그 전에는 Django 함수를 기반으로 한 개발이 주로 되어 있었습니다. 그러다 새로운 프로젝트를 진행하게 되었고, 그때, 회사의 다른 개발자분들과 새로운 프로젝트를 진행할 스펙과 기술에 대해 정하던 중에, Front-end와 Back-end를 분리해야 할 필요가 있어서 알아보게 되었고,  Back-end에서는 Django rest framework를, Front-end에서는 Angularjs를 알게 되어 그때 Back-end 기술의 일환으로 Django Rest framework의 사용 결정했습니다.

먼저, 기존의 코드와 Django rest framework를 사용해서 어떤 식으로 단점을 보안 하였는지 말씀드리도록 하겠습니다. 첫번째로, 아래 보이는 코드가 기존 프로젝트에서 view를 return 할때 사용한 코드 입니다. 함수 기반의 view를 사용해서 작업을 했었습니다.

def test_view(self, request):
    return render_to_response('test', {})

이 view 방식의 단점은 front-end코드와 back-end코드가 맞물려 있다는 단점과, html 코드가 아닌 다른 코드를 리턴해야 할 필요가 있을 경우, 모든 코드의 수정이 필요하다는 것입니다.

아래의 코드는 json을 리턴하고 싶을 때 사용했던 코드 입니다. (더 좋은 방법이 있을수도 있으나, 간단하게 parameter를 받아서 작업을 하였습니다.)

def test_view(self,request):
    # Return JSON type
    if request.GET.get('type') == 'json':
        return HttpResponse(data, content_type='application/json')
    return render_to_response('test', {})

이 또한 마찬가지로 http method를 분기 처리 할때 똑같이 발생하는 문제였습니다.

def test_view(self,request):
    # Return JSON type
    if request.method == 'POST':
        blahblah...
    if request.method == 'GET':
        return render_to_response('test', {})

이러한 문제가 발생함에 따라 처음에는 저기까지는 뭐 노가다로 하면 할수 있다고 생각이 들수도 있긴 합니다만,  Front-end가 점점 복잡 해짐에 따라서 **“ID로 필터를 걸어주세요.! 리턴되는 데이터가 너무 많으니 pagination처리해서 UI에 보여주고 싶어요!” **라는 요구 사항들이 들어오고, 많은 요구 사항에 대해 어떻게 유연하게 처리해 줄수 있지? 라고 생각을 하게 되었습니다.

오랜 고민 끝에, “그래! 장고에서 pagination을 지원하니, 일단 그걸 쓰도록 해볼까?? filter는 어떻게하지?? 아 그냥 손수 parameter 받아서 구현하지 뭐 얼마나 많아 지겠어." 라는 생각이 들어 구현 작업에 나섰습니다.

그리하여 나온 이 아래 코드가 filter와 장고에서 지원하는 pagination을 적용시킨 코드 입니다. 보시면 try…except 같은 코드가 pagination처리할때 보여서, 코드를 보기에도 좋아 보이지 않죠?

from django.core.paginator import Paginator
def test_view(self,request):
    model_list = Model.objects.all()
    # Filter
    some_filter = request.GET.get('some_filter')
    if some_filter:
        model_list = model_list.filter(some=some_filter)
    ...
    # Pagination
    paginator = Paginator(model_list, 25)
        page = request.GET.get('page')
        try:
            models = paginator.page(page)
        except PageNotAnInteger:
            models = paginator.page(1)
    return render_to_response('test', {'models': model})

좋아.. 여기까지는 우리가 잘 버텼어!! 근데, 데이터 validation처리도 해야 하는데 어떻게 하지? 장고 Form을 이용하자!!! 음.. 그럼 사용할때 마다 Form이 필요한거겠네..라는 생각이 안 들 수가 없는데요. 그래서 고안한 방법이 아래의 코드입니다.

# test/forms.py
from django.forms import ModelForm
from test.models import Model

class TestForm(ModelForm):
    class Meta:
        model = Model
    def clean():
        cleaned_data = super(TestForm, self).clean()
        # add validation code
        return cleaned_data

# test.views.py
from django.core.paginator import Paginator
from test.forms import TestForm

def test_view(self,request):
    # Return JSON type
    if request.method == 'POST':
        form = TestForm(request.POST)
        if form.isvalid():
            form.save()
        else:
            blahblah
    if request.method == 'GET':

    return render_to_response('test', {'models': model})

어찌어찌하여 여기까지는 잘 버텼다고 생각했지만 알고보니 아래와 같은 더 많은 요구사항들이 있었습니다.

  • 권한 별로 데이터의 제한도 두고 싶고 한데 이런건 어떻게 처리하지?
  • URL별로 권한은 설정 할수 있는데, 데이터의 대한 권한은 처리할수 있을까??
  • 날짜 데이터를 내려줄때 해당 유저의 로컬 시간으로 보내줄수는 있을까?

위의 문제점들을 해결하기 위하여 이 작업을 또하긴 싫은데 어떻게하지 생각하다가 이 단점들을 보안할 수 있고, 나중에도 사용할수 있는 Django rest framework를 알게 되었고, 저희의 문제점을 해결해 줄 수 있을 거 같다는 생각이 들었습니다.

2. Django Rest Framework에 대하여

Django rest framework 의 장점을 말씀드리자면 다음과 같습니다.

  • API 지원
  • 다양한 authentication 지원
  • class 기반의 구현 방식
  • 유저 권한 별 데이터 제한 가능
  • 다양한 return 타입 제공 e.g) json, csv, excel…
  • custom을 통해 확장 가능

위의 장점들에서도 알 수 있듯이 Django Rest Framework는 API레벨도 잘 지원하면서, Back-end와 Front-end를 분리할수 있고, Django와도 잘 맞는 프레임워크였고, 더불어, 새로운 프로젝트를 진행하는 시점에서 새로운 기술을 사용하는건 새로운 도전이라고 생각해서 사용하기로 결정하였습니다.

그래서 저희가 처음으로 도입한 방법은 문서에 있는 class 베이스의 generics package에 있는 것을 상속 받아서 구현하는 것이였습니다. 이게 처음에 짠 코드 입니다. 위에 비교해서 엄청 깔끔 해졌죠?

# test/urls.py
urlpatterns = patterns(
    'test.views',
    url(r'^/api/tests/(?P<pk>\d+)', TestDetailView.as_view()),
    url(r'^/api/tests', TestListView.as_view()),
)

# test/views.py
class TestListView(generics.ListCretateView):
    queryset = Model.objects.all()
    serializer_class = ModelSerializer

class TestDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Model.objects.all()
    serializer_class = ModelSerializer

이러한 방법으로 개발의 속도를 높이고 있던 와중에 점점 url이 많아짐에 따라 view도 많아지고, copy&paste 되는 코드도 많아 관리상의 이슈가 생기게 되었습니다. 이건 샘플로 url이 많아졌을 경우에 view를 이용해 컨트롤 할수 있는 방법입니다.

# test/urls.py

urlpatterns = patterns(
    'test.views',
    url(r'^/api/tests/(?P<pk>\d+)/pets/(?P<pk>\d+)', PetsDetailView.as_view()),
    url(r'^/api/tests/(?P<pk>\d+)/pets', PetsListView.as_view()),
    url(r'^/api/tests/(?P<pk>\d+)', TestDetailView.as_view()),
    url(r'^/api/tests', TestListView.as_view()),
)

# test/views.py
class TestListView(generics.ListCretateView):
    queryset = Model.objects.all()
    serializer_class = ModelSerializer

class TestDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Model.objects.all()
    serializer_class = ModelSerializer

class PetsListView(generics.ListCretateView):
    queryset = Pet.objects.all()
    serializer_class = PetSerializer

class PetsDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Pet.objects.all()
    serializer_class = PetSerializer

또한, url(N) === view(N) 개 생기는 관리상의 이슈와 확장성의 이슈를 해결할수 있는 좋은 방법이 없을까 고민하던 와중에, Django rest framework에 Viewset과 Router를 통해 view들의 집합을 만들수 있다는 것을 알게 되었고, 이것을 통해 위와 같은 관리상 이슈는 해결 할 수 있다는 것을 알았습니다.

이 아래 코드가 viewset과 router를 사용했을 때의 코드 입니다. 깔끔하게 정리 되었죠??

# test/urls.py

router = SimpleRouter()
router.register(r'/pets', PetViewSet)
router.register(r'/', TestViewSet)

urlpatterns = patterns(
    url(r'^/api/tests', include(router.urls))
)

# test/viewsets.py
class PetViewSet(viewsets.ModelViewSet):
    serializer_class = PetSerializer
    queryset = Pet.objects.all()

class TestViewSet(viewsets.ModelViewSet):
    serializer_class = ModelSerializer
    queryset = Model.objects.all()

3. 추가적으로 해결해야할 문제들

이제 해결해야 할 문제는 페이징 처리와 filter에 관련된 내용인데요. 이 문제도 Django rest framework에서 제공하는 pagination_class와 filter_fields, search_fields ordering_fields를 쉽고 보기 좋게 해결 할 수 있었습니다.

  • pagination_class 는 페이징 처리시에 참조가 되는 class의 이름을 세팅하는 변수이고, 세팅이 되어 있을 경우에 리턴 되는 데이터를 페이징 처리해서 보여줍니다. 페이징 처리를 하였을 때랑 페이징 처리를 하지 않았을 경우, return 되는 데이터의 구조가 달라지게 됩니다. (중요!!)

    • pagination_class가 세팅 되어 있을 때 return되는 데이터의 구조

      { count: 1000, results: […] }

    • pagination_class가 세팅 되지 않았을 때 return되는 데이터의 구조

      […]

  • filter_backends 변수는 해당 class로 요청이 들어왔을 때 사용할 filter들을 정의할때 사용됩니다. 밑에 예제에서 보면 저는 3개의 필터(SearchFilter, OrderingFilter, DjangoFilterBackend)를 사용 했습니다.

    • SearchFilter
      • 사용법: url에 search parameter에 들어오는 값에 대한 filter설정을 할수 있습니다. e.g) www.superbong.com?search=daniel
      • search_fields에는 search에서 들어온 값이 필터될 fields를 설정합니다.
    • OrderingFilter
      • 사용법: url에  ordering에 들어오는 이름으로 ordering을 설정할수 있습니다. (* 앞에 -가 있을 경우 내림차순으로 ordering이 정렬됩니다.)  e.g) www.superbong.com?ordering=-id
      • ordering_fields에는 ordering으로 사용할 fields를 설정하게 됩니다.
    • DjangoFilterBackend
      • 이것은 url에서 fields이름으로 filter를 사용하고 싶을때, 사용하는 filter입니다.
      • url 뒤에 parameter로 column=값 을 설정하여 사용하면 됩니다.
      • filter_fields를 통해 column으로 들어올 fields를 설정할수 있습니다.
      • 그 외에 들어오는 column에 대해서는 무시 됩니다.

짜잔!! 이 아래에 있는 코드가 filter와 ordering을 썼을때의 최종 코드 입니다.

# test/utils.py
class LargePagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 10000

# test/viewsets.py
class PetViewSet(viewsets.ModelViewSet):
    pagination_class = LargePagination
    serializer_class = PetSerializer
    queryset = Pet.objects.all()
    filter_backends = (filters.SearchFilter, filters.OrderingFilter, filters.DjangoFilterBackend, )
    filter_fields = ('filter1', )
    serach_fields = ('id', 'name', )
    ordering_fields = ('id', 'created_at',)
    ordering = ('-id')

Django rest framework의 또 다른 장점이라고 할수 있는건, 권한 및 유저 별 데이터의 포맷을 변경 하기 용이 하다는 것입니다. 저희는 Serializer를 사용해서 포맷이나 데이터의 제한을 두고 있습니다. 아래와 같이 Serializer를 통해 client에 들어오는 데이터(to_internal_value)와 나가는 데이터(to_representation)의 변환을 쉽게 할수 있습니다.

  • to_internal_value
    • POST, PUT과 같이 데이터 변경이 있을 때 client에서 들어오는 데이터를 저장 전에 핸들링 할수 있는 함수입니다.
  • to_representation
    • GET, POST, PUT과 같이 데이터 변경이 있고 난 후에 client에 값을 변환해서 보여줄 경우 사용하는 함수 입니다.
# test/serializers.py
class AllDataReturnSerializer(serializers.ModelSerializer):
    class Meta:
        model = Model
        fields = '__all__'

    def to_representation(self, val):
        return super(AllDataReturnSerializer, self).to_representation(val)

    def to_internal_value(self, val):
        return super(AllDataReturnSerializer, self).to_internal_value(val)

class SomeDataReturnSerializer(serializers.ModelSerializer):
    class Meta:
        model = Model
        fields = (
            'some1',
            'some2',
            'some3',
        )

# test/viewsets.py
class PetViewSet(viewsets.ModelViewSet):
    
    def get_serializer_class(self):
        if return_all_data:
            return AllDataReturnSerlaizer
        elif return_some_data:
            return SomeDataReturnSerializer

4. 끝으로

Django rest framework를 쓰면서 느끼는 점은 완벽하지 않아도 규약이나 규칙을 통해 개발자의 개발 시간과 가독성을 올리는 데 한 몫을 하였습니다.

그와 별개로 인제 Django rest framework를 사용 한 후에 저희가 다음으로 신경 쓰는 부분은 테스트 인거 같습니다. 개발자가 많아지므로 인해서 관리해야 하는 코드도 많아지고, 변경을 했을 때의 생각치도 않은 곳에서 발생하는 오류가 점점 커지면서 소모되는 리소스의 양도 점점 많아지고 있습니다.

그래서 지금까지는 Back-end API레벨단의 테스트만 진행하고 있는데, 앞으로의 고민은 Front-end에 대한 코드가 점점 커지면서, 테스트에 대한 Needs가 필요한데, 이것에 대한 처리를 어떻게 해야 좋을지, 화면단의 테스트 코드까지 테스트코드로 진행해야 할지 고민해야 하는 큰 문제 인거 같습니다. 이에 앞으로도 많은 고민이 수반되어야 할 것 같습니다.

You May Also Like

버즈빌, 아마도 당신이 원하던 회사!

지원하기