- 발행일
Django CBV vs FBV — 클래스형 뷰와 함수형 뷰 비교
Django CBV vs FBV — 클래스형 뷰와 함수형 뷰 비교
Django는 MTV(Model-Template-View) 패턴을 기반으로 하며, View는 두 가지 방식으로 작성할 수 있습니다:
- FBV (Function-Based View): 함수로 뷰를 작성
- CBV (Class-Based View): 클래스로 뷰를 작성
FBV — 함수형 뷰
from django.shortcuts import render, get_object_or_404, redirect
from .models import Post
from .forms import PostForm
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
return render(request, 'post_detail.html', {'post': post})
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
form.save()
return redirect('post_list')
else:
form = PostForm()
return render(request, 'post_form.html', {'form': form})
- HTTP 메서드(
GET,POST)를if로 직접 분기 - 코드 흐름이 한눈에 보임
- 커스터마이징이 자유로움
CBV — 클래스형 뷰
Django가 제공하는 제네릭 뷰를 상속해서 사용합니다.
from django.views.generic import DetailView, CreateView
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
class PostDetailView(DetailView):
model = Post
template_name = 'post_detail.html'
class PostCreateView(CreateView):
model = Post
form_class = PostForm
template_name = 'post_form.html'
success_url = reverse_lazy('post_list')
- CRUD 패턴을 몇 줄로 구현 가능
- 상속과 Mixin으로 코드 재사용
- 내부 동작을 이해해야 커스터마이징 가능
주요 제네릭 CBV 목록
| 뷰 클래스 | 주요 속성/메서드 | 용도 |
|---|---|---|
TemplateView | template_name, get_context_data() | 단순 템플릿 렌더링 |
ListView | model, queryset, paginate_by | 목록 페이지 |
DetailView | model, pk_url_kwarg | 상세 페이지 |
CreateView | form_class, success_url, form_valid() | 생성 폼 |
UpdateView | form_class, success_url, form_valid() | 수정 폼 |
DeleteView | success_url | 삭제 확인 |
LoginView | form_class, redirect_field_name | 로그인 |
PasswordChangeView | success_url | 비밀번호 변경 |
각 CBV의 상세 속성은 ccbv.co.uk 에서 확인할 수 있습니다.
메서드 오버라이드 패턴
CBV는 필요한 메서드만 오버라이드해서 동작을 바꿉니다.
class PostCreateView(CreateView):
model = Post
form_class = PostForm
success_url = reverse_lazy('post_list')
def form_valid(self, form):
# 저장 전 작성자를 현재 로그인 유저로 지정
form.instance.author = self.request.user
return super().form_valid(form)
def get_context_data(self, **kwargs):
# 템플릿에 추가 컨텍스트 전달
context = super().get_context_data(**kwargs)
context['title'] = '새 글 작성'
return context
FBV vs CBV 비교
| 항목 | FBV | CBV |
|---|---|---|
| 코드 가독성 | 직관적, 위에서 아래로 읽힘 | 제네릭 뷰 구조 이해 필요 |
| 코드 양 | CRUD 패턴 반복 많음 | 제네릭 활용 시 매우 짧음 |
| 커스터마이징 | 자유도 높음 | 메서드 오버라이드 방식 |
| 재사용성 | 낮음 (헬퍼 함수 별도 구성) | Mixin으로 높은 재사용성 |
| 학습 난이도 | 낮음 | 상대적으로 높음 |
| HTTP 메서드 분기 | if request.method == 'POST' | get(), post() 자동 분리 |
언제 무엇을 쓸까?
FBV가 적합한 경우
- 단순하고 일회성인 뷰 (API 연동, 특수 리다이렉트 등)
- 복잡한 조건 분기가 많은 뷰
- Django를 처음 배우는 시점
CBV가 적합한 경우
- CRUD 패턴이 반복되는 뷰가 많을 때
- 여러 뷰에서 공통 로직을 Mixin으로 묶고 싶을 때
LoginRequiredMixin,PermissionRequiredMixin등 내장 Mixin을 활용할 때
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
class PostCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
permission_required = 'blog.add_post'
model = Post
form_class = PostForm
success_url = reverse_lazy('post_list')
Mixin 순서가 이 순서인 데는 이유가 있습니다. 파이썬 MRO상 왼쪽에 있는 클래스의 dispatch()가 먼저 실행되므로, LoginRequiredMixin이 맨 앞이어야 로그인 확인이 권한 확인보다 먼저 걸립니다. 순서를 바꿔도 에러는 안 나요 — 어떤 검사가 먼저 도는지가 조용히 달라질 뿐입니다. CBV에서 "동작은 하는데 의도와 다른" 종류의 문제는 대부분 이렇게 MRO에 숨어 있습니다.
FBV에서 같은 처리를 하려면 데코레이터를 직접 쌓아야 합니다:
from django.contrib.auth.decorators import login_required, permission_required
@login_required
@permission_required('blog.add_post')
def post_create(request):
...
실무에서 CBV가 이겼던 순간과, 물렸던 순간
이 비교는 원론적인데, 실제 프로젝트에서 어느 쪽이 얼마나 이득이었는지는 구체적인 상황에서 갈렸습니다.
CBV가 확실히 이긴 경우 — 공개 화면을 관리자 화면으로 복제할 때
표준데이터 관계도 화면을 관리자용으로도 만들어야 했습니다. 로직은 완전히 같고 권한과 레이아웃, 노출 범위만 달랐어요.
CBV라서 이렇게 끝났습니다.
class StdDataGraphView(TemplateView):
active_only = True # 확장점을 클래스 속성으로 승격
def get_queryset(self):
qs = Dataset.objects.all()
return qs.filter(state="ACT") if self.active_only else qs
class AdminStdDataGraphView(StaffOnlyMixin, StdDataGraphView):
active_only = False # 관리자는 심사중·보류도 조회
extra_context = {"section": "admin"}
관리자 뷰 네 종을 이 패턴으로 한 번에 만들었습니다. FBV였다면 함수 네 개를 복사하고 각각에 데코레이터를 쌓고 필터 조건을 손으로 바꿨을 거예요.
여기서 배운 건 "차이나는 부분을 클래스 속성으로 올려두면 상속이 설정처럼 동작한다" 는 점입니다. active_only를 메서드 안에 하드코딩했으면 get_queryset()을 통째로 오버라이드해야 했고, 그러면 로직이 두 벌이 됩니다.
CBV에 물렸던 경우 — 상속이 오타까지 물려준다
반대 사례도 있습니다. 목록 성능을 잡으려고 get_queryset()에 prefetch_related를 넣었어요.
class DatasetBaseView(...):
def get_queryset(self):
return super().get_queryset().prefetch_related("metadata")
문제는 이 "metadata"가 Dataset에 존재하지 않는 관계명이었다는 겁니다. QuerySet은 지연 평가라 선언 시점엔 아무 검증도 없이 통과하고, 실제로 순회하는 순간 AttributeError가 납니다. 그리고 이 부모 뷰를 상속한 다운로드 뷰 네 개가 그 잘못된 prefetch를 전부 물려받아 같이 죽었어요. 스택 트레이스는 오타를 낸 부모 파일이 아니라 자식 뷰 쪽에서 시작해서, 원인을 찾는 데 한참 걸렸습니다.
FBV였으면 각 함수가 자기 쿼리를 갖고 있으니 틀린 함수 하나만 죽었을 문제입니다. CBV의 재사용성은 "좋은 것도 물려주지만 틀린 것도 물려준다" 는 뜻이었어요. 한동안 이 사건을 "전량 조회 경로의 메모리 부담" 때문이라고 잘못 기억하고 있었는데, 커밋을 다시 확인하니 원인은 없는 관계명이었습니다. 자세한 건 없는 관계를 prefetch하고 있었다와 N+1 정리 글의 반대 사례에 적었습니다.
get_queryset()에 부작용을 넣지 말 것
CBV에서 특히 하기 쉬운 실수가 하나 더 있습니다. 훅 메서드에 조회 외의 일을 넣는 거예요.
class UserTokenRequestView(LoginRequiredMixin, ListView):
def get_queryset(self):
ApiTokenRequest.sync_expired() # 조회 메서드가 DB를 쓴다
return ApiTokenRequest.objects.filter(user=self.request.user)
실제로 제가 쓴 코드입니다. 만료 상태를 화면 진입 시점에 맞추려는 의도였고 지금 규모에서는 잘 돌아가는데, 이름이 get_queryset인 메서드가 UPDATE를 실행합니다. 페이지네이션 때문에 여러 번 불릴 수도 있고요.
이 판단의 근거와 대가는 정책 토글·만료 동기화 회고에 따로 적었습니다. FBV였다면 함수 안에 순서대로 적혀서 눈에 띄었을 텐데, CBV에서는 훅 안에 숨으면 잘 안 보입니다.
결국 나눈 기준
| 상황 | 선택 |
|---|---|
| 같은 화면의 변형이 여럿 (공개/관리자, 필터만 다름) | CBV — 차이를 클래스 속성으로 |
| CRUD가 반복 | CBV — 제네릭 + Mixin |
| 조건 분기가 복잡하고 한 번만 쓰는 화면 | FBV — 흐름이 그대로 보임 |
| 여러 모델을 조합해 한 화면을 만드는 대시보드 | FBV 또는 TemplateView + 명시적 메서드 |
원칙 하나만 남기면 — "이 화면의 변형이 앞으로 생길 것 같은가" 입니다. 생길 것 같으면 CBV로 두고 차이날 지점을 클래스 속성으로 미리 뽑아두는 게 나중에 제일 싸게 먹혔어요.
정리
FBV와 CBV는 둘 다 유효한 선택이며, 하나의 프로젝트에서 혼용해도 괜찮습니다.
- FBV: 읽기 쉽고 빠르게 작성 가능. 복잡한 로직에 강함.
- CBV: 반복 패턴 제거에 강함. Mixin 조합으로 확장성 좋음.
- CBV를 쓸 땐 차이나는 값을 클래스 속성으로 올려두고, 훅 메서드(
get_queryset등)에는 조회 외의 일을 넣지 않기. - 상속은 최적화도 물려줍니다. 부모 쿼리셋에 붙인 것이 모든 자식 경로에서 옳은지 확인하세요.
실무에서는 단순 CRUD는 CBV로, 비즈니스 로직이 복잡한 뷰는 FBV로 작성하는 경우가 많습니다.