수학과의 좌충우돌 프로그래밍

[Django] CBV (1) CBV 와 Base Views 본문

웹프로그래밍/Django

[Django] CBV (1) CBV 와 Base Views

ssung.k 2019. 8. 6. 03:23

CBV 란?

기존에는 views 를 함수로 사용해왔습니다. 이는 Function Based View , 줄여서 FBV 라고 합니다. 그렇다면 함수 외에 다른 것으로 views 를 작성할 수는 없을까요? 가능합니다. views는 사실 함수가 아닌 Callable Objects,즉 호출가능한 객체면 문제 없습니다. 함수도 호출가능한 객체 중 하나이기 때문에 사용할 수 있는 것이죠.

따라서 다른 호출가능한 객체인 클래스로도 views 를 구성할 수 있습니다. 이는 Class Based View, 줄여서 CBV 라고 합니다.

CBV 의 장점

우선 클래스의 장점을 모두 사용할 수 있습니다. 상속, 오버라이딩 등등 여러 방식으로 코드의 효율을 극대화 할 수 있습니다. FBV 로 views 를 작성하다보면 생각보다 중복되는 코드들을 많이 볼 수 있습니다. SNS 를 예로 들어도 글쓰기, 댓글쓰기. 글지우기 , 댓글지우기 등등 벌써 부터 겹치는 기능들이 많이 있습니다. 이를 중심이 되는 클래스를 하나 만들고 상황에 맞게 상속함으로서 코드의 길이를 줄이고 효율을 높일 수 있는 것이죠. 이 밖의 여러 장점들은 실습을 하면서 몸으로 느끼도록 하겠습니다.

CBV 의 단점

CBV 는 이미 많은 부분이 정해져있습니다. 이는 장점으로 작용하여 바로 사용할 수 도 있지만 이미 정해져있는 부분들을 제대로 이해하고 있지 않는다면 이를 커스텀하기에 애로사항을 겪을 수 있습니다. 또한 클래스의 상속 등과 같은 여러 파이썬 문법에 대한 선행 지식이 선행되어야 하니 초심자에게는 어려울 수 있습니다. FBV 를 어느 정도 숙지하신 후에 CBV 를 사용하실 것을 권장합니다.

 

base views

기본 토대가 되는 base views 에 대해서 알아보도록 하겠습니다. 전체적인 소소코드는 소스코드 전문보기 에서 확인 할 수 있습니다.

 

View

모든 CBV의 부모 클래스로서 기본적인 기능들이 응집되어 있습니다. 그렇게 때문에 이를 직접 쓸 일이 거의 없지만 다른 클래스들을 사용함으로서 간접적으로 항상 사용되고 있다고 보시면 됩니다.

class View:
    """
    Intentionally simple parent class for all views. Only implements
    dispatch-by-method and simple sanity checking.
    """

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

    def __init__(self, **kwargs):
        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in kwargs.items():
            setattr(self, key, value)

    @classonlymethod
    def as_view(cls, **initkwargs):
        """Main entry point for a request-response process."""
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())
        return view

    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

    def http_method_not_allowed(self, request, *args, **kwargs):
        logger.warning(
            'Method Not Allowed (%s): %s', request.method, request.path,
            extra={'status_code': 405, 'request': request}
        )
        return HttpResponseNotAllowed(self._allowed_methods())

    def options(self, request, *args, **kwargs):
        """Handle responding to requests for the OPTIONS HTTP verb."""
        response = HttpResponse()
        response['Allow'] = ', '.join(self._allowed_methods())
        response['Content-Length'] = '0'
        return response

    def _allowed_methods(self):
        return [m.upper() for m in self.http_method_names if hasattr(self, m)]
  • init

    뒤에 나올 as_view 에 인자를 넘겨주게 되면 그 인자가 *kwargs 로 넘어와 이 dict 를 순회하며 해당 설정을 해주게 됩니다. 예를 들어 template_name 값을 지정했다고 하면 인스턴스 변수를 생성하여 이에 값을 할당하고 해당 변수가 필요할 경우 클래스 변수보다 인스턴스 변수를 먼저 참조합니다.

  • as_view

    as_view 메소드는 view를 하나 만들어서 이를 return 합니다. 즉 함수가 하나 만들어지는거죠. CBV도 결국 기본적인 동작은 함수를 베이스로 합니다. 클래스를 정의한 후에 이에 해당하는 함수를 만들어 그 함수를 사용하는 방식으로 말이죠. 뒤 쪽에서 as_view 를 자주 보실 수 있을 겁니다.

  • dispatch

    dispatch 메소드는 as_view 메소드 내부에서 호출됩니다. request method 를 확인하고 http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] 안에 있는 정상적인 method 라면 request 에서 이를 가져오게 됩니다. 만약 정상적인 method 가 아니라면 http_method_not_allowed 를 호출합니다 .

 

 

TemplateView

  • 실습

전체적인 코드를 살펴보기 전에 어떻게 사용되고 있는지 예시를 먼저 보도록 하겠습니다. 다른 부분의 코드는 모두 동일하다고 생각하고 FBV 로 먼저 코드를 짠 후 그에 대한 CBV 를 알아보도록 하겠습니다.

# FBV

from django.shortcuts import render

def index(request):
    context = {
        'one': 1,
        'myUser' : request.user
    }
    return render(request, 'core/index.html', context)

여기서 3가지를 주의깊게 보도록 하겠습니다. 첫번째로 template 의 이름, 두 번째로 정적인 context, 마지막으로 동적은 context 입니다.

 

# CBV

from django.views.generic import TemplateView

index = TemplateView.as_view(
    template_name = "core/index.html",
	  extra_context= {'one': 1},
)

template_name 을 통해서 template의 이름을, extra_context 로 다음과 같이 정적인 context 를 지정해 줄 수 있습니다. 하지만 request.user 처럼 동적인 경우에는 extra_context 가 아니라 get_context_data 함수를 사용해야 합니다.

 

# CBV

class MyTemplateView(TemplateView):
    template_name= "core/index.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({
            'one': 1,
            'myUser' : self.request.user,
        })
        return context

index = MyTemplateView.as_view()

이를 위해서 MyTemplateView 라는 class 를 정의하고 TemplateView 를 상속받았습니다. get_context_data 함수는 상속해준 TemplateViewget_context_data 함수를 호출해 context 를 받아오고 이에 원하는 내용을 추가해 return 해주었습니다. 이 때 request 가 아닌 self.request 인 점 유의해주시면 되겠습니다.

 

  • TemplateView 살펴보기

    위의 내용이 이해가 안되는게 정상입니다. 우리가 위에서 사용했던 변수, 함수들이 어떻게 정의되어 있는지 알 수 없으니깐요. TemplateView 가 어떻게 구성되어있는지 확인해보도록 하겠습니다.

    class TemplateView(TemplateResponseMixin, ContextMixin, View):
        """
        Render a template. Pass keyword arguments from the URLconf to the context.
        """
        def get(self, request, *args, **kwargs):
            context = self.get_context_data(**kwargs)
            return self.render_to_response(context)
    

    TemplateResponseMixin,ContextMixin,View 다음 3개의 class 에 대해서 상속을 받았습니다.

     

    • TemplateResponseMixin

      class TemplateResponseMixin:
          """A mixin that can be used to render a template."""
          template_name = None
          template_engine = None
          response_class = TemplateResponse
          content_type = None
      
          def render_to_response(self, context, **response_kwargs):
              """
              Return a response, using the `response_class` for this view, with a
              template rendered with the given context.
              Pass response_kwargs to the constructor of the response class.
              """
              response_kwargs.setdefault('content_type', self.content_type)
              return self.response_class(
                  request=self.request,
                  template=self.get_template_names(),
                  context=context,
                  using=self.template_engine,
                  **response_kwargs
              )
      
          def get_template_names(self):
              """
              Return a list of template names to be used for the request. Must return
              a list. May not be called if render_to_response() is overridden.
              """
              if self.template_name is None:
                  raise ImproperlyConfigured(
                      "TemplateResponseMixin requires either a definition of "
                      "'template_name' or an implementation of 'get_template_names()'")
              else:
                  return [self.template_name]
      

      template 의 기본적인 설정들, template의 이름, rendering 하는 과정 등을 담당하게 됩니다. 하지만 여기서 context 를 rendering 해주는 코드는 있지만 context 를 받아주는 과정은 보이지 않습니다. 이는 아래 ContextMixin 에서 해줍니다.

       

    • ContextMixin

      class ContextMixin:
          """
          A default context mixin that passes the keyword arguments received by
          get_context_data() as the template context.
          """
          extra_context = None
      
          def get_context_data(self, **kwargs):
              kwargs.setdefault('view', self)
              if self.extra_context is not None:
                  kwargs.update(self.extra_context)
              return kwargs
      

      위에 실습에서 봤듯이 정적인 context 에 대해서는 extra_context 변수를 사용하면 되고 동적으로 사용되야 하는 경우에는 get_context_data 함수를 재정의 함으로서 사용하면 됩니다.

 

RedirectView

  • 실습

위와 마찬가지로 다른 부분의 코드는 모두 동일하다고 생각하고 FBV 로 먼저 코드를 짠 후 그에 대한 CBV 를 알아보도록 하겠습니다.

# FBV

from django.shortcuts import redirect

def index2(reqeust):
    return redirect('index')

크게 의미 없는 코드이지만 실습을 위해 작성하였습니다.

 

# CBV

index2 = RedirectView.as_view(pattern_name='index')

pattern_name 을 통해서 해당 이름을 가진 url 로 redirect 할 수 있습니다.

 

  • RedirectView 살펴보기

    이번에도 마찬가지로 RedirectView 를 보면서 전체적인 코드의 흐름을 이해해보도록 하겠습니다.

    class RedirectView(View):
        """Provide a redirect on any GET request."""
        permanent = False
        url = None
        pattern_name = None
        query_string = False
    
        def get_redirect_url(self, *args, **kwargs):
            """
            Return the URL redirect to. Keyword arguments from the URL pattern
            match generating the redirect request are provided as kwargs to this
            method.
            """
            if self.url:
                url = self.url % kwargs
            elif self.pattern_name:
                url = reverse(self.pattern_name, args=args, kwargs=kwargs)
            else:
                return None
    
            args = self.request.META.get('QUERY_STRING', '')
            if args and self.query_string:
                url = "%s?%s" % (url, args)
            return url
    
        def get(self, request, *args, **kwargs):
            url = self.get_redirect_url(*args, **kwargs)
            if url:
                if self.permanent:
                    return HttpResponsePermanentRedirect(url)
                else:
                    return HttpResponseRedirect(url)
            else:
                logger.warning(
                    'Gone: %s', request.path,
                    extra={'status_code': 410, 'request': request}
                )
                return HttpResponseGone()
    
        def head(self, request, *args, **kwargs):
            return self.get(request, *args, **kwargs)
    
        def post(self, request, *args, **kwargs):
            return self.get(request, *args, **kwargs)
    
        def options(self, request, *args, **kwargs):
            return self.get(request, *args, **kwargs)
    
        def delete(self, request, *args, **kwargs):
            return self.get(request, *args, **kwargs)
    
        def put(self, request, *args, **kwargs):
            return self.get(request, *args, **kwargs)
    
        def patch(self, request, *args, **kwargs):
            return self.get(request, *args, **kwargs)
    
    
    • get

      get_redirect_url 을 호출하여 url 을 얻습니다. url을 얻지 못한다면 에러를 출력합니다. 제대로 url 을 얻었다면 permanent 에 의해서 분기 되기는 하지만 결과적으로 redirect 합니다.

    • get_redirect_url

      get_redirect_url 는 클래스 변수 url 을 통해 직접 url 을 받아오거나 pattern_name 을 통해 url 의 name 값을 받아왔으면 이를 return 해줍니다. 부가적으로 parmnent 값을 지정해주거나 query_string 을 지정해줄 수 있습니다.

    • head, post … patch

      아래 구현된 메소드 모두 get 함수를 실행시킵니다.

Comments