- 발행일
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하고 있었다2026-07-20없는 관계를 prefetch하고 있었다 — 다운로드 4종을 죽인 한 단어카탈로그 PNG 다운로드가 500을 뱉어서 들어갔는데, 원인은 성능 최적화라고 믿고 있던 prefetch_related의 인자 하나였습니다. Dataset에 없는 관계명이라 QuerySet 평가 시점에 AttributeError가 났고, 지연 평가 때문에 선언부가 아니라 버튼 클릭에서 터졌어요. 같은 파일 안에서 따옴표 표기가 갈려 grep이 한쪽만 잡은 이야기와, AST로 인자 478건을 전수 대조한 기록입니다.와 N+1 정리 글의 반대 사례2025-10-31Django N+1 쿼리 문제와 해결법Django ORM에서 자주 발생하는 N+1 쿼리 문제를 진단하고 select_related, prefetch_related로 해결하는 방법을 정리합니다.에 적었습니다.
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를 실행합니다. 페이지네이션 때문에 여러 번 불릴 수도 있고요.
이 판단의 근거와 대가는 정책 토글·만료 동기화 회고2026-06-13Open API 정책 토글의 캐시·만료 상태가 어긋나던 문제를 동기화한 회고관리자가 오퍼레이션을 토글했는데 TTL이 지나야 반영되던 캐시 갭, 그리고 만료된 토큰의 신청 상태가 "승인됨"으로 남던 stale 문제를 정리합니다. 캐시 즉시 무효화와 진입 시점 sync_expired로 "보이는 상태 = 실제 상태"를 맞춘 이야기예요.에 따로 적었습니다. FBV였다면 함수 안에 순서대로 적혀서 눈에 띄었을 텐데, CBV에서는 훅 안에 숨으면 잘 안 보입니다.
결국 나눈 기준
| 상황 | 선택 |
|---|---|
| 같은 화면의 변형이 여럿 (공개/관리자, 필터만 다름) | CBV — 차이를 클래스 속성으로 |
| CRUD가 반복 | CBV — 제네릭 + Mixin |
| 조건 분기가 복잡하고 한 번만 쓰는 화면 | FBV — 흐름이 그대로 보임 |
| 여러 모델을 조합해 한 화면을 만드는 대시보드 | FBV 또는 TemplateView + 명시적 메서드 |
원칙 하나만 남기면 — "이 화면의 변형이 앞으로 생길 것 같은가" 입니다. 생길 것 같으면 CBV로 두고 차이날 지점을 클래스 속성으로 미리 뽑아두는 게 나중에 제일 싸게 먹혔어요.
정리
FBV와 CBV는 둘 다 유효한 선택이며 하나의 프로젝트에서 혼용해도 괜찮습니다.
- FBV: 읽기 쉽고 빠르게 작성 가능. 복잡한 로직에 강함.
- CBV: 반복 패턴 제거에 강함. Mixin 조합으로 확장성 좋음.
- CBV를 쓸 땐 차이나는 값을 클래스 속성으로 올려두고 훅 메서드(
get_queryset등)에는 조회 외의 일을 넣지 않기. - 상속은 최적화도 물려줍니다. 부모 쿼리셋에 붙인 것이 모든 자식 경로에서 옳은지 확인하세요.
실무에서는 단순 CRUD는 CBV로, 비즈니스 로직이 복잡한 뷰는 FBV로 작성하는 경우가 많습니다.