내가 Django에서 Class Based View를 더 선호하는 이유

구경회·2022년 9월 4일
1
post-thumbnail

Django를 쓰다보면 많은 Controller(View)를 작성해야하고 그 때마다 이걸 class로 만드나? 단순한 function으로 만드나? 고민하게 된다. 최근에 이 선택의 기준에 관한 질문을 받기도 했고, 개인적인 생각을 글로 정리해보는게 좋을 거 같다는 생각이 들었다.

  • 편의를 위해 Django로 적었지만, Django-rest-framework에 좀 더 관련한 글이다.

나는 왜 CBV(Class-based View)를 더 선호하는가?

결론부터 말하자면 나는 CBV를 더 선호한다. 이유는 다음과 같다.

  1. HTTP 계층과 도메인 계층을 좀 더 깔끔하게 나눌 수 있다.
  2. 1.을 바탕으로 코드의 재사용성을 높일 수 있다.
  3. 구현의 세부사항을 숨기기에 용이하다.

물론, 짜고 다시 보지 않을 코드라면 무엇을 쓰든 상관이 없다. 이럴 때는 그냥 적당히 본인이 짜기 편한 쪽으로 골라잡는게 좋을 것이다. 하지만 코드베이스가 오래 살아남으면 살아남을수록 CBV가 주는 이점이 커진다고 생각한다.

다음과 같은 경우를 생각해보자.

@api_view(['GET', 'POST'])
def hello_world(request):
    if request.method == 'POST':
        return Response({"message": "Got some data!", "data": request.data})
    return Response({"message": "Hello, world!"})

DRF API Guide - @apiview에서 발췌
함수형 뷰의 경우 위처럼 request.method를 이용해 한 함수 내에서 분기하게 되는 경우가 잦다. 둘은 사실 완전히 다른 일을 하게 되고 수정하게 되면 어느 한 쪽만 수정하게 되는 경우가 잦다. 이 함수가 좀 더 발전한다면, 아마 다음과 같은 모습을 띄게 될 것이다.

@api_view(['GET', 'POST'])
def hello_world(request):
    if request.method == 'POST':
        if name not in request.data:
            raise ValidationError('Name is required')
        return Response({
            "message": "Got some data!",
            "data": request.data
        }, status=201)

    name = request.GET.get('name', 'world')
    return Response({"message": f"Hello, {name}!"})

... 시간이 지날수록 이 함수를 유지보수하기는 힘들어진다. 각기 다른 일(조회와 생성)을 한 곳에서 if, else를 나누어서 하게 되기 때문이다. 시간이 지나고 엔드포인트가 아주 많아지면 단순히 url을 찾는 것조차 힘들어지기 시작한다.

이제 조금 다른 예시를 생각해보자. Database 조회 기능이 있는 ListUsersGetUser 기능이 있는 엔드포인트를 상상해보자. 어떤 모습이어야하나? FBV로 구현하면 대략 다음과 같겠다.

def validate_admin(secret_key):
    return secret_key == 'admin'


@api_view(["GET"])
def article_list_create_api_view(request):
    is_admin = validate_admin(request.headers.get("X-Api-Key"))

    if is_admin:
        articles = Article.objects.all()
    else:
        articles = Article.objects.filter(is_public=True)
    serializer = ArticleSerializer(articles, many=True)
    return Response(serializer.data)


@api_view(["GET"])
def article_detail_api_view(request, pk):
    try:
        article = Article.objects.get(pk=pk)
    except Article.DoesNotExist:
        return Response({"error": {
            "code": 404,
            "message": "Article not found"
        }}, status=status.HTTP_404_NOT_FOUND)

    if article.is_public and not validate_admin(request.headers.get("X-Api-Key")):
        raise Article.DoesNotExist

어떤가, 합리적인가? 우선 다음과 같은 부분이 반복되는 걸 볼 수 있다.

is_admin = validate_admin(request.headers.get("X-Api-Key"))
if is_admin:
    articles = Article.objects.all()
else:
    articles = Article.objects.filter(is_public=True)

하단도 사실 같은 부분이라는 걸 알 수 있다. 하단을 다음과 같이 바꾸면 코드 중복이 여실히 드러난다.


@api_view(["GET"])
def article_detail_api_view(request, pk):
    try:
        is_admin = validate_admin(request.headers.get("X-Api-Key"))
        if is_admin:
            articles = Article.objects.all()
        else:
            articles = Article.objects.filter(is_public=True)
        article = articles.get(pk=pk)
    except Article.DoesNotExist:
        return Response({"error": {
            "code": 404,
            "message": "Article not found"
        }}, status=status.HTTP_404_NOT_FOUND)

    serializer = ArticleSerializer(article)
    return Response(serializer.data)

물론 둘이 완벽하게 동일하지는 않다. 하단의 경우 단 건 조회시 쿼리가 달라지기 때문이다. (is_public=True 조건이 추가된다) 하지만 지금은 코드 이야기를 하는 시간이니 지나가보자. 위 중복은 CBV를 쓴다면 다음과 같이 정리할 수 있다.

from rest_framework.generics import GenericAPIView
from rest_framework.mixins import RetrieveModelMixin, ListModelMixin

class ArticleView(ListModelMixin, RetrieveModelMixin, GenericAPIView):
    serializer_class = ArticleSerializer

    def get_queryset(self):
        if self._is_admin:
            return Article.objects.all()
        else:
            return Article.objects.filter(is_public=True)

    # protected

    @property
    def _is_admin(self):
        return validate_admin(self.request.query_params.get('X-Api-Key', ''))

훨씬 간단해졌을 뿐더러 이제 내 코드 내에서 http 계층과 관련된 부분을 볼 수 없다. 만약 HTTP 관련하여 세밀하게 조정해야한다면 그 부분에 대한 함수(가령, list)만 오버라이드하여 조절하면 된다. 내 비즈니스 로직(쿼리 조건)과 도메인과 관계 없는 바깥 부분 (HTTP 응답)을 격리한 것이다.

또한 _is_admin처럼 구현의 아주 아랫부분을 숨겨 코드를 읽을 때 훨씬 수월하다. get_queryset처럼 자주 읽고 만지게 되는 함수 내의 추상화 수준이 비슷하기 때문이다. 추상화의 수준이 왜 비슷한 것끼리 뭉쳐있어야 하는지에 대해서는 SLASH 21 - 실무에서 바로 쓰는 Frontend Clean Code을 참고하자.

참고자료

profile
즐기는 거야

0개의 댓글