발행일

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 목록

뷰 클래스주요 속성/메서드용도
TemplateViewtemplate_name, get_context_data()단순 템플릿 렌더링
ListViewmodel, queryset, paginate_by목록 페이지
DetailViewmodel, pk_url_kwarg상세 페이지
CreateViewform_class, success_url, form_valid()생성 폼
UpdateViewform_class, success_url, form_valid()수정 폼
DeleteViewsuccess_url삭제 확인
LoginViewform_class, redirect_field_name로그인
PasswordChangeViewsuccess_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 비교

항목FBVCBV
코드 가독성직관적, 위에서 아래로 읽힘제네릭 뷰 구조 이해 필요
코드 양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로 작성하는 경우가 많습니다.