
from django.views.generic.base 에 선언된 view 및 mixin 에 대해 알아보려고 합니다.
Django 를 처음 접할 때 view 종류가 정말 다양하다고 느꼈고, 각각의 적절한 용도와 구조를 몰라 고생했던 경험이 있습니다.
base.py 에 선언된 view 부터 차례대로 모든 generic view 에 대해 용도와 구조를 파악해보겠습니다.
A default context mixin that passes the keyword arguments received by get_context_data() as the template context.
class ContextMixin:
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
get_context_data 메소드를 통해 전달받은 파라미터에서 view 객체를 확인하고, 비어있다면 self 객체를 호출해 업데이트합니다.
get_context_data 메소드는 TemplateVIew 가 ContextMixin 을 상속하여, context 데이터를 template 에 전달할 때 사용됩니다.
일반적인 Django CBV 호출 흐름에서는 get_context_data()가 항상 자기 자신(self) 을 기준으로 동작하므로, kwargs 안에 "view" 키가 미리 들어 있을 일이 거의 없습니다. 따라서 setdefault("view", self)가 실행돼도 대개 self가 그대로 저장됩니다.
그러나 몇 가지 특수 상황에서는 "view"가 다른 View 인스턴스로 넘어올 수 있습니다.
| 상황 | 예시 | 왜 필요한가? |
|---|---|---|
| 하위 뷰가 상위 뷰의 템플릿·컨텍스트를 재사용 | 커스텀 관리 사이트에서 ParentView.get_context_data(view=self)로 자식 뷰의 컨텍스트를 합칠 때 | 부모 템플릿 안에서 “원래 뷰”에 접근해야 할 수 있어서 |
| 뷰를 위임(delegate)·포함(include)하는 컴포넌트 형식 | Form-wizard, Tab/Step 뷰 등에서 “현재 탭 뷰”와 “컨테이너 뷰”를 분리 | 템플릿이 실제 탭 뷰의 속성·메서드를 써야 할 때 |
| 테스트·메타 프로그래밍 | 테스트 코드에서 OtherView().get_context_data(view=mock_view) | 의도적으로 다른 뷰를 주입하여 동작 확인 |
이처럼 매우 드문 케이스지만, Django Core는 라이브러리·확장 코드가 자유롭게 ‘뷰 주입’을 할 수도 있다는 점을 고려해 setdefault로 “이미 있으면 건드리지 않는다”는 방식을 채택했습니다. 덕분에 평범한 사용자는 아무것도 신경 쓸 필요 없고, 고급 사용자/라이브러리는 필요할 때만 "view"를 덮어쓸 수 있습니다.
kwargs.setdefault(key, default)는 파이썬 dict 객체의 메서드로, 다음 두 가지 일을 한 번에 처리합니다.
key가 이미 있으면 아무것도 바꾸지 않고 그 값을 그대로 돌려줍니다.key가 없으면 key: default 쌍을 새로 넣고, 그 default 값을 돌려줍니다.Intentionally simple parent class for all views. Only implements dispatch-by-method and simple sanity checking.
class View:
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)
View 의 생성자는 URLconf 에서 호출될 때 실행되며, 추가적인 파라미터를 전달받을 수 있습니다.
HTTP 요청이 들어올 때마다 인스턴스가 새로 만들어지고 init()이 실행됩니다.
setattr() 메소드를 통해 추가적인 파라미터를 View 객체의 속성으로 추가합니다.
setattr(obj, name, value)
파이썬 내장 함수로 객체 obj에 문자열 name으로 된 속성을 동적으로 만들거나(없으면 생성) 값 value를 할당합니다. 사실상 obj.name = value 와 동일하지만, 이름을 문자열로 받아 런타임에 속성 이름을 결정할 수 있다는 점이 다릅니다.
| 매개변수 | 의미 |
|---|---|
obj | 속성을 추가‧변경할 대상 객체 |
name | 속성 이름(문자열) |
value | 저장할 값 |
| 반환값 | 항상 None |
⚠️ name이 읽기 전용 속성(property의 setter 없음 등)·슬롯 미정의·setattr에서 막혀 있으면 AttributeError가 납니다.
@classproperty
def view_is_async(cls):
handlers = [
getattr(cls, method)
for method in cls.http_method_names
if (method != "options" and hasattr(cls, method))
]
if not handlers:
return False
is_async = iscoroutinefunction(handlers[0])
if not all(iscoroutinefunction(h) == is_async for h in handlers[1:]):
raise ImproperlyConfigured(
f"{cls.__qualname__} HTTP handlers must either be all sync or all "
"async."
)
return is_async
view_is_async는 @classproperty이므로 호출 순간의 “클래스 객체”(MyView)가 그대로 cls에 전달됩니다. 인스턴스가 아니라 자기 자신(서브클래스) 입니다.
view_is_async 가 실행될 때, classproperty 데코레이터로부터 cls 를 전달받기 때문에 view_is_async 가 사용하는 cls 는 함수가 속해있는 클래스(상속한 클래스) 자기자신입니다.
handler 는 (컴프리헨션으로 초기화하고 있습니다.) cls, 즉 자기자신의 클래스에 선언된 속성 중에 http_method_names 에 등록된 http 메소드를 위한 메소드를 가져옵니다.
handler 는 http method 에 대응하는 함수를 저장하는 리스트입니다.
handler 가 모두 async 또는 sync 로 통일해서 선언된 경우에 view_is_async 는 각각 True/False 를 반환합니다. True 라면 해당 view 가 async 로, False 라면 sync 로 선언되었다는 의미입니다.
handler 가 async/sync 로 통일되어 있지 않다면 ImproperlyConfigured 에러를 발생시킵니다.
classproperty 데스크립터(descriptor)이다.
# from django.utils.functional import classproperty
class classproperty:
"""
Decorator that converts a method with a single cls argument into a property
that can be accessed directly from the class.
"""
def __init__(self, method=None):
self.fget = method
def __get__(self, instance, cls=None):
return self.fget(cls)
def getter(self, method):
self.fget = method
return self
클래스 속성을 읽으면 파이썬이 자동으로 __get__(None, <해당 클래스>) 를 호출해 주기 때문에, 데코레이터 내부 함수는 인수를 받지 않아도 cls 인자로 “자기 자신 클래스” 를 건네받아 실행된다.
@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(
"The method name %s is not accepted as a keyword argument "
"to %s()." % (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)
self.setup(request, *args, **kwargs)
if not hasattr(self, "request"):
raise AttributeError(
"%s instance has no 'request' attribute. Did you override "
"setup() and forget to call super()?" % cls.__name__
)
return self.dispatch(request, *args, **kwargs)
view.view_class = cls
view.view_initkwargs = initkwargs
# __name__ and __qualname__ are intentionally left unchanged as
# view_class should be used to robustly determine the name of the view
# instead.
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
view.__annotations__ = cls.dispatch.__annotations__
# Copy possible attributes set by decorators, e.g. @csrf_exempt, from
# the dispatch method.
view.__dict__.update(cls.dispatch.__dict__)
# Mark the callback if the view class is async.
if cls.view_is_async:
markcoroutinefunction(view)
return view
View.as_view()가 필요한가?URLconf는 “호출가능 객체(callable)”를 요구하지만, CBV(Class-Based View)는 클래스입니다.
따라서 클래스를 한 번 감싸 “뷰 함수”(callable)로 바꿔 URLconf 에 넘기는 역할을 as_view 메소드가 수행합니다.
as_view()를 쓰나?as_view()가 둘 사이의 어댑터 역할.view 인스턴스를 URLConf 에 넘기는 path("view/", View() 와 같은 형태를 기대할 수도 있지만, 많은 문제점이 있습니다.
View는 __call__()을 구현하지 않으므로 View() 자체는 함수처럼 실행될 수 없습니다.
path()가 기대하는 것은 def view(request, …) 형태의 콜러블입니다.
View()는 URLconf가 로드될 때 단 한 번 만들어집니다.
모든 요청이 이 하나의 객체를 공유하게 되면
| 결과 | 설명 |
|---|---|
| 상태 오염 | self.request, self.args, self.kwargs 등 요청별 속성이 덮어써져 동시 요청에서 뒤섞임 |
| 스레드 안전성 문제 | 멀티스레드 환경에서 레이스 컨디션 발생 가능 |
| 메모리 누수 | 요청별로 붙는 임시 속성이 객체에 계속 남을 수 있음 |
setup()·dispatch() 호출 흐름이 깨진다as_view()는 self.setup() → self.dispatch()를 자동 실행해 주지만, 직접 인스턴스를 넘기면 누가, 언제 이 메서드를 호출해야 할지 정의되어 있지 않습니다.
URL 캡처 파라미터(pk, slug 등)도 dispatch()에 전달돼야 하는데 흐름이 끊깁니다.
template_name 같은 키워드 설정 주입이 불가능as_view(template_name="foo.html")처럼 URLconf 단계에서 속성을 주입할 통로가 사라집니다.
인스턴스를 미리 만들어 버리면, 뷰마다 별도 서브클래스를 작성해야 하는 번거로움이 생깁니다.
as_view()는 @csrf_exempt 등 dispatch()에 붙은 데코레이터 속성을 복사하고,
view_is_async를 검사해 코루틴으로 마킹합니다.
인스턴스를 직통으로 넘기면 이런 프레임워크 레벨 후처리가 모두 생략됩니다.
def setup(self, request, *args, **kwargs):
"""Initialize attributes shared by all view methods."""
if hasattr(self, "get") and not hasattr(self, "head"):
self.head = self.get # ①
self.request = request # ②
self.args = args # ③
self.kwargs = kwargs # ④
View.setup() 메서드의 역할setup()은 as_view()가 인스턴스를 만든 직후에 호출되어,
모든 HTTP 메서드(get(), post() …)에서 공통으로 써야 하는 값들을 한곳(view 클래스)에 초기화해 두는 “준비 단계”입니다.
setup() 및 view() 에서 사용되는 self 는 View 클래스(혹은 서브클래스) 자체를 의미합니다.
| 번호 | 동작 | 의미 |
|---|---|---|
| ① | HEAD → GET 위임뷰가 get()만 정의하고 head()를 정의하지 않은 경우, HEAD 요청이 들어오면 get()을 대신 호출하도록 메서드 포인터를 옮깁니다.HTTP 규격에서 HEAD는 GET과 동일한 헤더를 돌려주되 바디만 생략하므로, 대부분의 단순 뷰는 get() 하나만으로 충분합니다. | |
| ② | self.request에 HttpRequest 객체를 저장 | 이후 모든 핸들러가 요청 정보를 공유할 수 있게 됨 |
| ③ | self.args에 URL 패턴의 위치 인자(보통 거의 없음) 저장 | |
| ④ | self.kwargs에 URL 캡처 변수(pk, slug 등)를 저장 | 핸들러 메서드나 다른 헬퍼 메서드에서 손쉽게 접근 가능 |
setup()이 필요한가?get(), post() 각각에서 self.request = request처럼 같은 코드를 반복할 필요가 없습니다.setup()을 오버라이드하면, 모든 요청 메서드 전에 공통 로직(예: 권한 체크, 공용 쿼리셋 주입)을 쉽게 넣을 수 있습니다. 반드시 super().setup(...) 호출만 잊지 않으면 됩니다.class LoggedInOnlyView(View):
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
if not request.user.is_authenticated:
raise PermissionDenied()
즉, setup()은 “뷰 인스턴스가 실제 요청과 엮이기 시작하는 첫 지점”으로,
이곳에서 공용 속성을 세팅하고, HEAD 요청 처리를 자동 보완해 Django CBV의 일관된 동작을 보장합니다.
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)
request 의 http method 에 적절한 handler 함수를 실행합니다.
as_view 의 view() 에서 self.dispatch() 를 반환하는 것은 request 에 적절한 handler 함수를 찾아 실행하는 것입니다.
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},
)
response = HttpResponseNotAllowed(self._allowed_methods())
if self.view_is_async:
async def func():
return response
return func()
else:
return response
view 가 지원하지 않는 http method 임을 알리고, 지원하는 http method 목록을 제공합니다
def options(self, request, *args, **kwargs):
"""Handle responding to requests for the OPTIONS HTTP verb."""
response = HttpResponse()
response.headers["Allow"] = ", ".join(self._allowed_methods())
response.headers["Content-Length"] = "0"
if self.view_is_async:
async def func():
return response
return func()
else:
return response
OPTIONS는 “이 엔드포인트가 어떤 HTTP 메서드를 지원하는가?”를 묻는 표준 HTTP 메서드입니다.
지원하는 http method 목록을 제공합니다.
def _allowed_methods(self):
return [m.upper() for m in self.http_method_names if hasattr(self, m)]
http_method_names 에 등록된 http method 중에 sel, 즉 자기자신 클래스에 구현되어 있는 메소드의 목록을 제공합니다.
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]
Django template 을 렌더링해 TemplateResponse 객체로 돌려주는 공통 기능을 모아 둔 CBV 믹스인입니다.
한 뷰가 어떤 템플릿 엔진을 사용해 HTML을 렌더링할지 명시적으로 지정하고 싶을 때 이 값을 설정합니다.
Django 1.8부터 Django 템플릿 언어(DTL) 외에도 Jinja2, 다른 커스텀 엔진을 동시에 등록해 쓸 수 있도록 “다중 템플릿 엔진” 체계를 도입했습니다.
python
복사편집
# settings.py
TEMPLATES = [
{ # 0번 엔진 ─ 'django' 기본 엔진
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": ["templates"],
"APP_DIRS": True,
"OPTIONS": {...},
"NAME": "django", # ← alias (생략 시 'django')
},
{ # 1번 엔진 ─ Jinja2
"BACKEND": "django.template.backends.jinja2.Jinja2",
"DIRS": ["jinja2_templates"],
"OPTIONS": {...},
"NAME": "jinja2", # ← alias
},
]
여러 엔진을 동시에 두면, 동일한 경로의 파일명이 겹칠 때 어느 엔진을 써야 하는지 애매해질 수 있습니다.
이때 CBV 수준에서 template_engine = "jinja2"처럼 지정하면 충돌 없이 원하는 엔진으로 고정됩니다.
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)
get 요청에 대해 context 를 포함한 적절한 template 을 반환합니다.
TemplateResponseMixin 을 상속하여 render_to_response 메소드를 통해 설정된 template 과 context 가 포함되어 있는 TemplateResponse 를 반환합니다.
View 클래스를 상속하기 때문에 get 이외의 http method 에 대해 메소드를 추가할 수 있지만, 그런 경우에 다른 generic view 를 사용하는 편이 간결하고 유지보수가 쉽기 때문에 굳이 추가할 필요는 없습니다.
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)
RedirectView는 특정 URL(또는 URL 패턴 이름) 로 HTTP 리다이렉트를 수행하기 위한 가장 단순한 CBV(Class-Based View)입니다.
GET뿐 아니라 POST·PUT 등 거의 모든 메서드를 같은 로직으로 처리해 “요청이 오면 무조건 이곳으로 보내라”는 용도로 사용됩니다.
고정 URL 사용
if self.url:
url = self.url % kwargs # URL에 패턴 매개변수 삽입
패턴 이름 사용
elif self.pattern_name:
url = reverse(self.pattern_name, args=args, kwargs=kwargs)
둘 다 없는 경우 → None 반환
query_string=True 이면 원본 요청의 ?key=value를 그대로 이어 붙임.
args = self.request.META.get("QUERY_STRING", "")
if args and self.query_string:
url = "%s?%s" % (url, args)
url이 있으면permanent=True → HttpResponsePermanentRedirect(301)permanent=False → HttpResponseRedirect(302)url이 없으면 410 Gone 응답을 반환하고 로그에 Warning 기록.